Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Alexander_Naumov:SLE-12:Update
openCryptoki.1904
ocki-3.1_08_0001-Add-a-pkcscca-tool-to-help-mig...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File ocki-3.1_08_0001-Add-a-pkcscca-tool-to-help-migrate-cca-private-token.patch of Package openCryptoki.1904
From afb086ce22bd1ff4d0f1cf0768dfff3c03424096 Mon Sep 17 00:00:00 2001 From: Joy Latten <jmlatten@linux.vnet.ibm.com> Date: Thu, 28 Aug 2014 00:36:43 -0500 Subject: [PATCH 1/2] Add a pkcscca tool to help migrate cca private token objects from v2(encrypted with cca hardware) to v3 (encrypted in software) Signed-off-by: Joy Latten <jmlatten@linux.vnet.ibm.com> --- configure.in | 1 + usr/sbin/Makefile.am | 6 +- usr/sbin/pkcscca/Makefile.am | 14 + usr/sbin/pkcscca/pkcscca.c | 661 +++++++++++++++++++++++++++++++++++++++++++ usr/sbin/pkcscca/pkcscca.h | 49 ++++ 5 files changed, 730 insertions(+), 1 deletion(-) create mode 100644 usr/sbin/pkcscca/Makefile.am create mode 100644 usr/sbin/pkcscca/pkcscca.c create mode 100644 usr/sbin/pkcscca/pkcscca.h Index: opencryptoki/configure.in =================================================================== --- opencryptoki.orig/configure.in +++ opencryptoki/configure.in @@ -818,6 +818,7 @@ AC_CONFIG_FILES([Makefile usr/Makefile \ usr/sbin/pkcsslotd/Makefile \ usr/sbin/pkcsconf/Makefile \ usr/sbin/pkcsicsf/Makefile \ + usr/sbin/pkcscca/Makefile \ usr/sbin/pkcscca_migrate/Makefile \ usr/sbin/pkcsep11_migrate/Makefile \ usr/lib/pkcs11/methods/Makefile \ Index: opencryptoki/usr/sbin/Makefile.am =================================================================== --- opencryptoki.orig/usr/sbin/Makefile.am +++ opencryptoki/usr/sbin/Makefile.am @@ -11,4 +11,8 @@ if ENABLE_PKCSEP11_MIGRATE PKCSEP11_MIGRATE_DIR = pkcsep11_migrate endif -SUBDIRS = pkcsslotd pkcsconf $(PKCSICSF_DIR) $(PKCSCCA_MIGRATE_DIR) $(PKCSEP11_MIGRATE_DIR) +if ENABLE_CCATOK +PKCSCCA_DIR = pkcscca +endif + +SUBDIRS = pkcsslotd pkcsconf $(PKCSICSF_DIR) $(PKCSCCA_MIGRATE_DIR) $(PKCSEP11_MIGRATE_DIR) $(PKCSCCA_DIR) Index: opencryptoki/usr/sbin/pkcscca/Makefile.am =================================================================== --- /dev/null +++ opencryptoki/usr/sbin/pkcscca/Makefile.am @@ -0,0 +1,14 @@ +sbin_PROGRAMS=pkcscca + +pkcscca_CFLAGS = -DSTDLL_NAME=\"pkcscca\" +pkcscca_LDFLAGS = -lcrypto -ldl + +# Not all versions of automake observe sbinname_CFLAGS +AM_CFLAGS = -DSTDLL_NAME=\"pkcscca\" + +pkcscca_SOURCES = ../../lib/pkcs11/common/p11util.c \ + ../../lib/pkcs11/common/sw_crypt.c \ + ../../lib/pkcs11/common/log.c \ + pkcscca.c + +INCLUDES = -I. -I../../include/pkcs11 -I../../lib/pkcs11/common Index: opencryptoki/usr/sbin/pkcscca/pkcscca.c =================================================================== --- /dev/null +++ opencryptoki/usr/sbin/pkcscca/pkcscca.c @@ -0,0 +1,661 @@ +/* + * Licensed materials - Property of IBM + * + * pkcscca - A tool for PKCS#11 CCA token. + * Currently, only migrates CCA private token objects from CCA cipher + * to using a software cipher. + * + * + * Copyright (C) International Business Machines Corp. 2014 + * + */ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <getopt.h> +#include <termios.h> +#include <dlfcn.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <linux/limits.h> +#include <openssl/evp.h> +#include <pkcs11types.h> + +#include "sw_crypt.h" +#include "pkcscca.h" + +void (*CSNBDEC)(); +int v_flag = 0; + +int compute_hash(int hash_type, int buf_size, char *buf, char *digest) +{ + EVP_MD_CTX md_ctx; + unsigned int result_size; + int rc; + + switch (hash_type) { + case HASH_SHA1: + rc = EVP_DigestInit(&md_ctx, EVP_sha1()); + break; + case HASH_MD5: + rc = EVP_DigestInit(&md_ctx, EVP_md5()); + break; + default: + return -1; + break; + } + + if (rc != 1) { + fprintf(stderr, "EVP_DigestInit() failed: rc = %d\n", rc); + return -1; + } + + rc = EVP_DigestUpdate(&md_ctx, buf, buf_size); + if (rc != 1) { + fprintf(stderr, "EVP_DigestUpdate() failed: rc = %d\n", rc); + return -1; + } + + result_size = EVP_MD_CTX_size(&md_ctx); + rc = EVP_DigestFinal(&md_ctx, (unsigned char *)digest, &result_size); + if (rc != 1) { + fprintf(stderr, "EVP_DigestFinal() failed: rc = %d\n", rc); + return -1; + } + + return 0; +} + +int cca_decrypt(unsigned char *in_data, unsigned long in_data_len, + unsigned char *out_data, unsigned long *out_data_len, + unsigned char *init_v, unsigned char *key_value) +{ + long return_code, reason_code, rule_array_count, length; + unsigned char chaining_vector[18]; + unsigned char rule_array[256]; + + length = in_data_len; + rule_array_count = 1; + memcpy(rule_array, "CBC ", 8); + + CSNBDEC(&return_code, &reason_code, NULL, NULL, key_value, + &length, in_data, init_v, &rule_array_count, + rule_array, chaining_vector, out_data); + + if (return_code != 0) { + fprintf(stderr, "CSNBDEC (DES3 DECRYPT) failed: return_code=%ld reason_code=%ld\n", return_code, reason_code); + return -1; + } + *out_data_len = length; + return 0; +} + +int reencrypt_private_token_object(unsigned char *data, unsigned long len, + unsigned char *new_cipher, + unsigned long *new_cipher_len, + unsigned char *masterkey) +{ + unsigned char *clear = NULL; + unsigned char des3_key[64]; + unsigned char sw_des3_key[3 * DES_KEY_SIZE]; + unsigned long clear_len; + CK_RV rc; + int ret; + + /* cca wants 8 extra bytes for padding purposes */ + clear_len = len + 8; + clear = (unsigned char *) malloc(clear_len); + if (!clear) { + fprintf(stderr, "malloc() failed: %s.\n", strerror(errno)); + ret =-1; + goto done; + } + + /* decrypt using cca des3 */ + memcpy(des3_key, masterkey, MASTER_KEY_SIZE); + ret = cca_decrypt(data, len, clear, &clear_len, "10293847", des3_key); + if (ret) + goto done; + + /* now encrypt using software des3 */ + memcpy(sw_des3_key, masterkey, 3 * DES_KEY_SIZE); + rc = sw_des3_cbc_encrypt(clear, clear_len, new_cipher, new_cipher_len, + "10293847", sw_des3_key); + if (rc != CKR_OK) + ret = -1; +done: + if (clear) + free(clear); + + return ret; +} + +int load_private_token_objects(unsigned char *data_store, + unsigned char *masterkey) +{ + FILE *fp1 = NULL, *fp2 = NULL; + unsigned char *buf = NULL; + unsigned char tmp[PATH_MAX], fname[PATH_MAX], iname[PATH_MAX]; + CK_BBOOL priv; + unsigned int size; + int rc, scount= 0, fcount = 0; + size_t read_size; + unsigned char *new_cipher; + unsigned long new_cipher_len; + + snprintf(iname, sizeof(iname), "%s/TOK_OBJ/OBJ.IDX", data_store); + + fp1 = fopen((char *)iname, "r"); + if (!fp1) + return -1; // no token objects + + while (!feof(fp1)) { + (void)fgets((char *)tmp, 50, fp1); + if (!feof(fp1)) { + tmp[strlen((char *)tmp) - 1] = 0; + + snprintf((char *)fname, sizeof(fname), "%s/TOK_OBJ/", + data_store); + strcat((char *)fname, (char *)tmp); + + fp2 = fopen((char *)fname, "r"); + if (!fp2) + continue; + + fread(&size, sizeof(unsigned int), 1, fp2); + fread(&priv, sizeof(CK_BBOOL), 1, fp2); + if (priv == FALSE) { + fclose(fp2); + continue; + } + + size = size - sizeof(unsigned int) - sizeof(CK_BBOOL); + buf = (unsigned char *) malloc(size); + if (!buf) { + fprintf(stderr, "Cannot malloc for object %s " + "(ignoring it).\n", tmp); + goto cleanup; + } + + read_size = fread((char *)buf, 1, size, fp2); + if (read_size != size) { + fprintf(stderr, "Cannot read object %s " + "(ignoring it).\n", tmp); + goto cleanup; + } + + new_cipher_len = size; + new_cipher = malloc(new_cipher_len); + if (!new_cipher) { + fprintf(stderr, "Cannot malloc space for new " + "cipher (ignoring object %s).\n", tmp); + goto cleanup; + } + + /* After reading the private token object, + * decrypt it using CCA des3 and then re-encrypt it + * using software des3. + */ + memset(new_cipher, 0, new_cipher_len); + rc = reencrypt_private_token_object(buf, size, + new_cipher, &new_cipher_len, + masterkey); + if (rc) + goto cleanup; + + fclose(fp2); + + /* now save the newly re-encrypted object back to + * disk in its original file. + */ + fp2 = fopen((char *)fname, "w"); + size = sizeof(unsigned int) + sizeof(CK_BBOOL) + + new_cipher_len; + (void)fwrite(&size, sizeof(unsigned int), 1, fp2); + (void)fwrite(&priv, sizeof(CK_BBOOL), 1, fp2); + (void)fwrite(new_cipher, new_cipher_len, 1, fp2); + rc = 0; + +cleanup: + if (fp2) + fclose(fp2); + if (buf) + free(buf); + if (new_cipher) + free(new_cipher); + + if (rc) { + if (v_flag) + printf("Failed to process %s\n", fname); + fcount++; + } else { + if (v_flag) + printf("Processed %s.\n", fname); + scount++; + } + } + } + fclose(fp1); + printf("Successfully migrated %d object(s).\n", scount); + + if (v_flag && fcount) + printf("Failed to migrate %d object(s).\n", fcount); + + return 0; +} + +int load_masterkey(char *mkfile, char *pin, char *masterkey) +{ + unsigned char des3_key[3 * DES_KEY_SIZE]; + unsigned char hash_sha[SHA1_HASH_SIZE]; + unsigned char pin_md5_hash[MD5_HASH_SIZE]; + unsigned char *cipher = NULL; + unsigned char *clear = NULL; + unsigned long cipher_len, clear_len; + int ret; + CK_RV rc; + FILE *fp = NULL; + + clear_len = cipher_len = MASTER_KEY_SIZE + SHA1_HASH_SIZE + (DES_BLOCK_SIZE - 1) & ~(DES_BLOCK_SIZE - 1); + + fp = fopen((char *)mkfile, "r"); + if (!fp) { + fprintf(stderr, "Could not open %s: %s\n", mkfile, + strerror(errno)); + return -1; + } + + cipher = malloc(cipher_len); + clear = malloc(clear_len); + if (cipher == NULL || clear == NULL) { + ret = -1; + goto done; + } + + ret = fread(cipher, cipher_len, 1, fp); + if (ret != 1) { + fprintf(stderr, "Could not read %s: %s\n", mkfile, + strerror(errno)); + ret = -1; + goto done; + } + + /* decrypt the masterkey */ + + ret = compute_md5(pin, strlen(pin), pin_md5_hash); + if (ret) { + fprintf(stderr, "Error calculating MD5 of PIN!\n"); + goto done; + } + + memcpy(des3_key, pin_md5_hash, MD5_HASH_SIZE); + memcpy(des3_key + MD5_HASH_SIZE, pin_md5_hash, DES_KEY_SIZE); + + rc = sw_des3_cbc_decrypt(cipher, cipher_len, clear, &clear_len, + (unsigned char *)"12345678", des3_key); + if (rc != CKR_OK) { + fprintf(stderr, "Error decrypting master key file after read"); + ret = -1; + goto done; + } + + /* + * technically should strip PKCS padding here but since I already know + * what the length should be, I don't bother. + * + * compare the hashes to verify integrity + */ + + ret = compute_sha1(clear, MASTER_KEY_SIZE, hash_sha); + if (ret) { + fprintf(stderr, "Failed to compute sha for masterkey.\n"); + goto done; + } + + if (memcmp(hash_sha, clear + MASTER_KEY_SIZE, SHA1_HASH_SIZE) != 0) { + fprintf(stderr, "%s appears to have been tampered!\n", mkfile); + fprintf(stderr, "Cannot migrate.\n"); + ret = -1; + goto done; + } + + memcpy(masterkey, clear, MASTER_KEY_SIZE); + ret = 0; + +done: + if (fp) + fclose(fp); + if (clear) + free(clear); + if (cipher) + free(cipher); + + return ret; +} + +int get_pin(char **pin, size_t *pinlen) +{ + struct termios old, new; + int nread; + char *buff = NULL; + size_t buflen; + int rc = 0; + + /* turn echoing off */ + if (tcgetattr(fileno(stdin), &old) != 0) + return -1; + + new = old; + new.c_lflag &= ~ECHO; + if (tcsetattr (fileno(stdin), TCSAFLUSH, &new) != 0) + return -1; + + /* read the pin + * Note: getline will allocate memory for buff. free it when done. + */ + nread = getline(&buff, &buflen, stdin); + if (nread == -1) { + rc = -1; + goto done; + } + + /* Restore terminal */ + (void) tcsetattr(fileno(stdin), TCSAFLUSH, &old); + + /* start a newline */ + printf("\n"); + fflush(stdout); + + /* Allocate PIN. + * Note: nread includes carriage return. + * Replace with terminating NULL. + */ + *pin = (unsigned char *)malloc(nread); + if (*pin == NULL) { + rc = -ENOMEM; + goto done; + } + + /* strip the carriage return since not part of pin. */ + buff[nread - 1] = '\0'; + memcpy(*pin, buff, nread); + /* don't include the terminating null in the pinlen */ + *pinlen = nread - 1; + +done: + if (buff) + free(buff); + + return rc; +} + +int verify_pins(char *data_store, char *sopin, unsigned long sopinlen, + char *userpin, unsigned long userpinlen) +{ + TOKEN_DATA td; + unsigned char fname[PATH_MAX]; + unsigned char pin_sha[SHA1_HASH_SIZE]; + FILE *fp = NULL; + int ret; + + /* read the NVTOK.DAT */ + snprintf(fname, PATH_MAX, "%s/NVTOK.DAT", data_store); + fp = fopen((char *)fname, "r"); + if (!fp) { + fprintf(stderr, "Could not open %s: %s\n", fname, + strerror(errno)); + return -1; + } + + ret = fread(&td, sizeof(TOKEN_DATA), 1, fp); + if (ret != 1) { + fprintf(stderr, "Could not read %s: %s\n", fname, + strerror(errno)); + ret = -1; + goto done; + } + + /* Now compute the SHAs for the SO and USER pins entered. + * Compare with the SHAs for SO and USER PINs saved in + * NVTOK.DAT to verify. + */ + + if (sopin != NULL) { + ret = compute_sha1(sopin, sopinlen, pin_sha); + if (ret) { + fprintf(stderr, "Failed to compute sha for SO.\n"); + goto done; + } + + if (memcmp(td.so_pin_sha, pin_sha, SHA1_HASH_SIZE) != 0) { + fprintf(stderr, "SO PIN is incorrect.\n"); + ret = -1; + goto done; + } + } + + if (userpin != NULL) { + ret = compute_sha1(userpin, userpinlen, pin_sha); + if (ret) { + fprintf(stderr, "Failed to compute sha for USER.\n"); + goto done; + } + + if (memcmp(td.user_pin_sha, pin_sha, SHA1_HASH_SIZE) != 0) { + fprintf(stderr, "USER PIN is incorrect.\n"); + ret = -1; + goto done; + } + } + ret = 0; + +done: + /* clear out the hash */ + memset(pin_sha, 0, SHA1_HASH_SIZE); + if (fp) + fclose(fp); + + return ret; +} + +void usage(char *progname) +{ + printf("usage:\t%s -h | -m v2objectsv3 [OPTIONS] \n", progname); + printf(" -h\t\t\t\tshow this help\n"); + printf(" -m=migration_type\t\tCurrently the only type of CCA "); + printf("migration\n\t\t\t\tsupported is v2objectsv3. v2objectsv3 "); + printf("migrates\n\t\t\t\tCCA private token objects from CCA "); + printf("encryption\n\t\t\t\t(used in v2)to software encryption "); + printf("(used in v3). \n\n"); + printf("Migrate options (with -m v2objectsv3):\n"); + printf(" -d, --datastore=DIRECTORY\tCCA token datastore location\n"); + printf(" -v, --verbose\t\t\tprovide more detailed output\n"); + + return; +} + +int main(int argc, char **argv) +{ + int ret, opt; + unsigned int m_flag = 0; + char *sopin = NULL, *userpin = NULL; + size_t sopinlen, userpinlen; + unsigned char masterkey[MASTER_KEY_SIZE]; + unsigned char *data_store = NULL; + unsigned char *m_type = NULL; + int data_store_len; + char fname[PATH_MAX]; + struct stat statbuf; + void *lib_csulcca; + + struct option long_opts[] = { + { "datastore", required_argument, NULL, 'd' }, + { "verbose", no_argument, NULL, 'v'}, + { 0, 0, 0, 0 } + }; + + int long_index; + while ((opt = getopt_long(argc, argv, "d:m:hv", long_opts, NULL)) != -1) { + switch (opt) { + case 'd': + data_store = strdup(optarg); + break; + + case 'h': + usage(argv[0]); + return 0; + + case 'm': + m_type = strdup(optarg); + break; + + case 'v': + v_flag++; + break; + + default: + usage(argv[0]); + return -1; + } + } + + if (m_type) { + if (memcmp(m_type, "v2objectsv3", strlen("v2objectsv3"))) { + fprintf(stderr, "unknown migration type\n"); + usage(argv[0]); + return -1; + } + } + + /* use default data_store if one is not given */ + if (data_store == NULL) { + data_store_len = strlen(TOK_DATASTORE); + data_store = malloc(data_store_len + 1); + if (data_store == NULL) { + fprintf(stderr, "malloc failed: %s\n",strerror(errno)); + return -1; + } + memset(data_store, 0, data_store_len + 1); + memcpy(data_store, TOK_DATASTORE, data_store_len); + } + + /* Verify that the data store is valid by looking for + * MK_SO, MK_USER, and TOK_OBJ/OBJ.IDX. + */ + + memset(fname, 0, PATH_MAX); + snprintf(fname, PATH_MAX, "%s/MK_SO", data_store); + if (stat(fname, &statbuf) != 0) { + fprintf(stderr, "Cannot find %s.\n", fname); + ret = -1; + goto done; + } + + memset(fname, 0, PATH_MAX); + snprintf(fname, PATH_MAX, "%s/MK_USER", data_store); + if (stat(fname, &statbuf) != 0) { + fprintf(stderr, "Cannot find %s.\n", fname); + ret = -1; + goto done; + } + + memset(fname, 0, PATH_MAX); + snprintf(fname, PATH_MAX, "%s/TOK_OBJ/OBJ.IDX", data_store); + if (stat(fname, &statbuf) != 0) { + fprintf(stderr, "Cannot find %s.\n", fname); + ret = -1; + goto done; + } + + /* If the OBJ.IDX is empty, then no objects to migrate. */ + if (statbuf.st_size == 0) { + printf("OBJ.IDX file is empty. Thus no objects to migrate.\n"); + goto done; + } + + if (v_flag) + printf("%s has an MK_SO, MK_USER and TOK/OBJ.IDX\n", + data_store); + + /* get the SO pin to authorize migration */ + printf("Enter the SO PIN: "); + fflush(stdout); + ret = get_pin(&sopin, &sopinlen); + if (ret != 0) { + fprintf(stderr, "Could not get SO PIN.\n"); + goto done; + } + + /* get the USER pin to authorize migration */ + printf("Enter the USER PIN: "); + fflush(stdout); + ret = get_pin(&userpin, &userpinlen); + + if (ret != 0) { + fprintf(stderr, "Could not get USER PIN.\n"); + goto done; + } + + /* Verify the SO and USER PINs entered. */ + ret = verify_pins(data_store, sopin, sopinlen, userpin, userpinlen); + if (ret) + goto done; + + lib_csulcca = dlopen(CCA_LIBRARY, (RTLD_GLOBAL | RTLD_NOW)); + if (lib_csulcca == NULL) { + fprintf(stderr, "dlopen(%s) failed: %s\n", CCA_LIBRARY, + strerror(errno)); + return -1; + } + + CSNBDEC = dlsym(lib_csulcca, "CSNBDEC"); + + /* Get the masterkey from MK_SO. + * This also helps verify that correct SO pin was entered. + */ + memset(masterkey, 0, MASTER_KEY_SIZE); + memset(fname, 0, PATH_MAX); + snprintf(fname, PATH_MAX, "%s/MK_SO", data_store); + ret = load_masterkey(fname, sopin, masterkey); + if (ret) { + fprintf(stderr, "Could not load masterkey from MK_SO.\n"); + goto done; + } + + if (v_flag) + printf("Successfully verified SO Pin.\n"); + + /* Get the masterkey from MK_USER. + * This also helps verift that correct USER pin was entered. + */ + memset(masterkey, 0, MASTER_KEY_SIZE); + memset(fname, 0, PATH_MAX); + snprintf(fname, PATH_MAX, "%s/MK_USER", data_store); + ret = load_masterkey(fname, userpin, masterkey); + if (ret) { + fprintf(stderr, "Could not load masterkey from MK_USER.\n"); + goto done; + } + + if (v_flag) + printf("Successfully verified USER Pin.\n"); + + /* Load all the private token objects and re-encrypt them + * using software des3, instead of CSNBENC. + */ + (void)load_private_token_objects(data_store, masterkey); + +done: + + if (sopin) + free(sopin); + if (userpin) + free(userpin); + if (data_store) + free(data_store); + + return ret; +} Index: opencryptoki/usr/sbin/pkcscca/pkcscca.h =================================================================== --- /dev/null +++ opencryptoki/usr/sbin/pkcscca/pkcscca.h @@ -0,0 +1,49 @@ +/* + * Licensed materials - Property of IBM + * + * pkcscca - A tool for PKCS#11 CCA token. + * Currently, only migrates CCA private token objects from using a + * CCA cipher to using a software cipher. + * + * Copyright (C) International Business Machines Corp. 2014 + * + */ + + +#ifndef __PKCSCCA_H_ +#define __PKCSCCA_H_ + +#define CCA_LIBRARY "libcsulcca.so" +#define TOK_DATASTORE "/var/lib/opencryptoki/ccatok" +#define MASTER_KEY_SIZE 64 +#define SHA1_HASH_SIZE 20 +#define MD5_HASH_SIZE 16 +#define DES_BLOCK_SIZE 8 +#define DES_KEY_SIZE 8 +#define compute_sha1(a,b,c) compute_hash(HASH_SHA1,b,a,c) +#define compute_md5(a,b,c) compute_hash(HASH_MD5,b,a,c) +#define HASH_SHA1 1 +#define HASH_MD5 2 + +/* from host_defs.h */ +#include "pkcs32.h" +typedef struct _TWEAK_VEC +{ + int allow_weak_des ; + int check_des_parity ; + int allow_key_mods ; + int netscape_mods ; +} TWEAK_VEC; + +typedef struct _TOKEN_DATA +{ + CK_TOKEN_INFO_32 token_info; + + CK_BYTE user_pin_sha[3 * DES_BLOCK_SIZE]; + CK_BYTE so_pin_sha[3 * DES_BLOCK_SIZE]; + CK_BYTE next_token_object_name[8]; + TWEAK_VEC tweak_vector; +} TOKEN_DATA; + + +#endif
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor