Merge pull request #108 from LedgerHQ/develop

Merge to master
master
Charles-Edouard de la Vergne 2 months ago committed by GitHub
commit 8b59bb8290
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,119 +1,18 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -2
BasedOnStyle: Google
IndentWidth: 4
Language: Cpp
ColumnLimit: 100
PointerAlignment: Right
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
#AlignConsecutiveMacros: true
AlignEscapedNewlines: Right
AlignOperands: true
AlignTrailingComments: true
#AllowAllArgumentsOnNextLine: false
#AllowAllConstructorInitializersOnNextLine: false
AlignConsecutiveMacros: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
SortIncludes: false
SpaceAfterCStyleCast: true
AllowShortCaseLabelsOnASingleLine: false
AllowAllArgumentsOnNextLine: false
AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
#AllowShortLambdasOnASingleLine: None
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
#IndentPPDirectives: BeforeHash
IndentWidth: 2
IndentWrappedFunctionNames: true
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: All
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
RawStringFormats:
- Delimiter: pb
Language: TextProto
BasedOnStyle: google
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
...
---

@ -0,0 +1,8 @@
# Checklist
<!-- Put an `x` in each box when you have completed the items. -->
- [ ] App update process has been followed <!-- See comment below -->
- [ ] Target branch is `develop` <!-- unless you have a very good reason -->
- [ ] Application version has been bumped <!-- required if your changes are to be deployed -->
<!-- Make sure you followed the process described in https://developers.ledger.com/docs/device-app/deliver/maintenance before opening your Pull Request.
Don't hesitate to contact us directly on Discord if you have any questions ! https://developers.ledger.com/discord -->

@ -18,4 +18,10 @@ jobs:
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
with:
upload_app_binaries_artifact: "compiled_app_binaries"
run_for_devices: '["nanos", "nanox", "nanosp"]'
ragger_tests:
name: Run ragger tests using the reusable workflow
needs: build_application
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1
with:
download_app_binaries_artifact: "compiled_app_binaries"

@ -0,0 +1,44 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
# Excluded path: add the paths you want to ignore instead of deleting the workflow
paths-ignore:
- '.github/workflows/*.yml'
- 'tests/*'
jobs:
analyse:
name: Analyse
strategy:
matrix:
sdk: [ "$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK", "$STAX_SDK" ]
#'cpp' covers C and C++
language: [ 'cpp' ]
runs-on: ubuntu-latest
container:
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
# CodeQL will create the database during the compilation
- name: Build
run: |
make BOLOS_SDK=${{ matrix.sdk }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -0,0 +1,25 @@
name: Run coding style check through reusable workflow
# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications.
#
# The presence of this workflow is mandatory as a minimal level of linting is required.
# You are however free to modify the content of the .clang-format file and thus the coding style of your application.
# We simply ask you to not diverge too much from the linting of the Boilerplate application.
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
jobs:
check_linting:
name: Check linting using the reusable workflow
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1
with:
source: './src'
extensions: 'h,c'
version: 11

@ -21,5 +21,3 @@ jobs:
guidelines_enforcer:
name: Call Ledger guidelines_enforcer
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1
with:
run_for_devices: '["nanos", "nanox", "nanosp"]'

@ -0,0 +1,29 @@
name: Misspellings checks
# This workflow performs some misspelling checks on the repository
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
# applications.
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
jobs:
misspell:
name: Check misspellings
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Check misspellings
uses: codespell-project/actions-codespell@v2
with:
builtin: clear,rare
check_filenames: true
ignore_words_list: ontop

@ -0,0 +1,43 @@
name: Checks on the Python client
# This workflow performs some checks on the Python client used by the Application tests
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
# applications.
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
jobs:
lint:
name: Client linting
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Installing PIP dependencies
run: |
pip install pylint
pip install -r tests/requirements.txt
- name: Lint Python code
run: |
pylint --rc tests/setup.cfg tests/application_client/
mypy:
name: Type checking
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Installing PIP dependencies
run: |
pip install mypy
pip install -r tests/requirements.txt
- name: Mypy type checking
run: |
mypy tests/application_client/

@ -0,0 +1,45 @@
name: Checks on the Tools client
# This workflow performs some checks on the Python client used by the cli tool
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
# applications.
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
jobs:
lint:
name: Client linting
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Installing PIP dependencies
run: |
sudo apt-get update && sudo apt-get install -y libpcsclite-dev
pip install pylint
pip install -r pytools/requirements.txt
- name: Lint Python code
run: |
pylint --rc pytools/setup.cfg pytools/
mypy:
name: Type checking
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Installing PIP dependencies
run: |
sudo apt-get update && sudo apt-get install -y libpcsclite-dev
pip install mypy
pip install -r pytools/requirements.txt
- name: Mypy type checking
run: |
mypy pytools/

@ -0,0 +1,50 @@
name: Unit testing with Codecov coverage checking
on:
workflow_dispatch:
push:
branches:
- master
- main
- develop
pull_request:
jobs:
job_unit_test:
name: Unit test
runs-on: ubuntu-latest
container:
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Build unit tests
run: |
cd unit-tests/
cmake -Bbuild -H. && make -C build && make -C build test
- name: Generate code coverage
run: |
cd unit-tests/
lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base && \
lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture && \
lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info && \
lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info && \
genhtml coverage.info -o coverage
- uses: actions/upload-artifact@v3
with:
name: code-coverage
path: unit-tests/coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./unit-tests/coverage.info
flags: unittests
name: codecov-app-openpgp
fail_ci_if_error: true
verbose: true

48
.gitignore vendored

@ -1,10 +1,38 @@
# Glyphs auto-generated
src/glyphs.*
# Build files
dep
obj
release
bin
debug
build
# Compilation files of the application
build/
# Legacy compilation output
bin/
debug/
# Temporary directory with snapshots taken during test runs
tests/snapshots-tmp/
# manual-tests
manual-tests/foo*
manual-tests/gnupg
# Unit tests and code coverage
unit-tests/build/
unit-tests/coverage/
unit-tests/coverage.info
# Fuzzing
fuzzing/build/
# Python
*.pyc[cod]
*.egg
__pycache__/
*.egg-info/
.eggs/
.python-version
venv/
gpg_backup
# Doxygen
doc/html
doc/latex
# Virtual env for sideload (macOS and Windows)
ledger/

@ -0,0 +1,11 @@
# Style file for mdl
# https://github.com/markdownlint/markdownlint/blob/main/docs/creating_styles.md
# Include all rules
all
# Disable specific rules
#exclude_rule 'MD012'
# Update rules configuration
rule 'MD013', :line_length => 120

@ -0,0 +1,11 @@
# markdownlint config file
# Use custom style file
style "#{File.dirname(__FILE__)}/.mdl.rb"
# MD005 - Inconsistent indentation for list items at the same level
# MD007 - Unordered list indentation
# MD014 - Dollar signs used before commands without showing output
# MD024 - Multiple headers with the same content
# MD041 - First line in file should be a top level header
rules "~MD005,~MD007,~MD014,~MD024,~MD041"

@ -0,0 +1,58 @@
# To install hooks, run:
# pre-commit install --hook-type pre-commit
# pre-commit install --hook-type commit-msg
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: mixed-line-ending
- id: check-added-large-files
- id: check-merge-conflict
- id: check-case-conflict
- repo: https://github.com/codespell-project/codespell
rev: v2.2.5
hooks:
- id: codespell
args: ['--ignore-words-list', 'ontop']
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v11.0.1
hooks:
- id: clang-format
types_or: [c]
- repo: https://github.com/Mateusz-Grzelinski/actionlint-py
rev: v1.6.26.11
hooks:
- id: actionlint
types_or: [yaml]
- repo: https://github.com/markdownlint/markdownlint
rev: v0.13.0
hooks:
- id: markdownlint
types_or: [markdown]
- repo: local
hooks:
# Python scripts
- id: pylint
name: Check python Client
# Only display messages, no score, disable few errors
entry: pylint -j 0 --rc tests/setup.cfg
language: system
types: [python]
files: '^tests\/.*$'
- id: pylint
name: Check python Tool
# Only display messages, no score, disable few errors
entry: pylint -j 0 --rc pytools/setup.cfg
language: system
types: [python]
files: '^pytools\/.*$'

@ -1,232 +1,135 @@
#*******************************************************************************
# Ledger App
# (c) 2016-2019 Ledger
# ****************************************************************************
# Ledger App OpenPGP
# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*******************************************************************************
-include Makefile.env
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ****************************************************************************
ifeq ($(BOLOS_SDK),)
$(error Environment variable BOLOS_SDK is not set)
endif
include $(BOLOS_SDK)/Makefile.defines
APP_LOAD_PARAMS=--appFlags 0x240 --path "2152157255'" --curve secp256k1 $(COMMON_LOAD_PARAMS)
include $(BOLOS_SDK)/Makefile.defines
ifeq ($(APPNAME),)
########################################
# Mandatory configuration #
########################################
# Application name
APPNAME = OpenPGP
endif
ifeq ($(APPNAME),OpenPGP)
GPG_MULTISLOT:=0
else ifeq ($(APPNAME),OpenPGP.XL)
GPG_MULTISLOT:=1
APPNAME:=OpenPGP.XL
else
$(error APPNAME ($(APPNAME)) is not set or unknown)
endif
ifeq ($(TARGET_NAME),TARGET_BLUE)
ICONNAME = images/icon_monero_blue.gif
else ifeq ($(TARGET_NAME),TARGET_NANOS)
ICONNAME = images/icon_pgp.gif
else
ICONNAME = images/icon_pgp_nanox.gif
endif
# Application version
APPVERSION_M = 2
APPVERSION_N = 2
APPVERSION_P = 2
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
APPVERSION_M:=1
APPVERSION_N:=4
APPVERSION_P:=4
APPVERSION:=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
SPECVERSION:="3.3.1"
DEFINES += $(OPENPGP_CONFIG)
DEFINES += OPENPGP_VERSION_MAJOR=$(APPVERSION_M) OPENPGP_VERSION_MINOR=$(APPVERSION_N) OPENPGP_VERSION_MICRO=$(APPVERSION_P)
DEFINES += OPENPGP_VERSION=$(APPVERSION)
DEFINES += OPENPGP_NAME=$(APPNAME)
DEFINES += SPEC_VERSION=$(SPECVERSION)
DEFINES += GPG_MULTISLOT=$(GPG_MULTISLOT)
#DEFINES += GPG_LOG
ifeq ($(TARGET_NAME),TARGET_BLUE)
DEFINES += UI_BLUE
else ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += UI_NANO_S
else
DEFINES += UI_NANO_X
DEFINES += GPG_SHAKE256
endif
################
# Default rule #
################
.PHONY: allvariants listvariants
all: default
mkdir -p release
cp -a bin/app.elf release/$(APPNAME).elf
cp -a bin/app.hex release/$(APPNAME).hex
cp -a debug/app.asm release/$(APPNAME).asm
cp -a debug/app.map release/$(APPNAME).map
# Application source files
APP_SOURCE_PATH += src
APP_SOURCE_FILES += $(BOLOS_SDK)/lib_cxng/src/cx_rsa.c
APP_SOURCE_FILES += $(BOLOS_SDK)/lib_cxng/src/cx_pkcs1.c
APP_SOURCE_FILES += ${BOLOS_SDK}/lib_cxng/src/cx_ram.c
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
listvariants:
@echo VARIANTS APPNAME OpenPGP OpenPGP.XL
allvariants:
make MULTISLOT=0 clean all
make MULTISLOT=1 clean all
############
# Platform #
############
#SCRIPT_LD := script.ld
ifneq ($(NO_CONSENT),)
DEFINES += NO_CONSENT
endif
# Application icons following guidelines:
# https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon
ICON_NANOS = icons/gpg_16px.gif
ICON_NANOX = icons/gpg_14px.gif
ICON_NANOSP = icons/gpg_14px.gif
ICON_STAX = icons/gpg_32px.gif
# Application allowed derivation curves.
# Possibles curves are: secp256k1, secp256r1, ed25519 and bls12381g1
# If your app needs it, you can specify multiple curves by using:
# `CURVE_APP_LOAD_PARAMS = <curve1> <curve2>`
CURVE_APP_LOAD_PARAMS = secp256k1
# Application allowed derivation paths.
# You should request a specific path for your app.
# This serve as an isolation mechanism.
# Most application will have to request a path according to the BIP-0044
# and SLIP-0044 standards.
# If your app needs it, you can specify multiple path by using:
# `PATH_APP_LOAD_PARAMS = "44'/1'" "45'/1'"`
PATH_APP_LOAD_PARAMS = "2152157255'"
# Setting to allow building variant applications
# - <VARIANT_PARAM> is the name of the parameter which should be set
# to specify the variant that should be build.
# - <VARIANT_VALUES> a list of variant that can be build using this app code.
# * It must at least contains one value.
# * Values can be the app ticker or anything else but should be unique.
VARIANT_PARAM = APPNAME
VARIANT_VALUES = OpenPGP
# Enabling DEBUG flag will enable PRINTF and disable optimizations
#DEBUG = 1
########################################
# Application custom permissions #
########################################
# See SDK `include/appflags.h` for the purpose of each permission
#HAVE_APPLICATION_FLAG_DERIVE_MASTER = 1
#HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1
#HAVE_APPLICATION_FLAG_BOLOS_SETTINGS = 1
#HAVE_APPLICATION_FLAG_LIBRARY = 1
########################################
# Application communication interfaces #
########################################
#ENABLE_BLUETOOTH = 1
#ENABLE_NFC = 1
########################################
# NBGL custom features #
########################################
#ENABLE_NBGL_QRCODE = 1
#ENABLE_NBGL_KEYBOARD = 1
ENABLE_NBGL_KEYPAD = 1
########################################
# Features disablers #
########################################
# These advanced settings allow to disable some feature that are by
# default enabled in the SDK `Makefile.standard_app`.
#DISABLE_STANDARD_APP_FILES = 1
#DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE = 1 # To allow custom size declaration
#DISABLE_STANDARD_APP_DEFINES = 1 # Will set all the following disablers
#DISABLE_STANDARD_SNPRINTF = 1
#DISABLE_STANDARD_USB = 1
DISABLE_STANDARD_WEBUSB = 1
#DISABLE_STANDARD_BAGL_UX_FLOW = 1
#DISABLE_DEBUG_LEDGER_ASSERT = 1
#DISABLE_DEBUG_THROW = 1
########################################
# Main app configuration #
########################################
DEFINES += OS_IO_SEPROXYHAL
DEFINES += HAVE_BAGL HAVE_SPRINTF
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
DEFINES += CUSTOM_IO_APDU_BUFFER_SIZE=\(255+5+64\)
DEFINES += HAVE_LEGACY_PID
DEFINES += USB_SEGMENT_SIZE=64
DEFINES += U2F_PROXY_MAGIC=\"MOON\"
#DEFINES += HAVE_IO_U2F HAVE_U2F
DEFINES += UNUSED\(x\)=\(void\)x
DEFINES += APPVERSION=\"$(APPVERSION)\"
DEFINES += HAVE_USB_CLASS_CCID
ifeq ($(NO_CXNG),)
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/include
endif
# RSA addition.
DEFINES += HAVE_RSA
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_rsa.c
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_pkcs1.c
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_utils.c
# RSA - End
ifeq ($(TARGET_NAME),TARGET_NANOX)
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
endif
DEFINES += HAVE_RSA
# Watchdog issue causing the device reset with long prime number computation
# DEFINES += WITH_SUPPORT_RSA4096
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
else
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
DEFINES += HAVE_GLO096
DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
DEFINES += HAVE_UX_FLOW
endif
# Enabling debug PRINTF
DEBUG = 0
ifneq ($(DEBUG),0)
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_PRINTF PRINTF=screen_printf
else
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
endif
DEFINES += PLINE="PRINTF(\"FILE:%s..LINE:%d\n\",__FILE__,__LINE__)"
else
DEFINES += PRINTF\(...\)=
DEFINES += PLINE\(...\)=
endif
##############
# Compiler #
##############
ifneq ($(BOLOS_ENV),)
$(info BOLOS_ENV=$(BOLOS_ENV))
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
else
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
endif
ifeq ($(CLANGPATH),)
$(info CLANGPATH is not set: clang will be used from PATH)
endif
ifeq ($(GCCPATH),)
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
endif
CC := $(CLANGPATH)clang
#CFLAGS += -O0 -gdwarf-2 -gstrict-dwarf
CFLAGS += -O3 -Os
#CFLAGS += -fno-jump-tables -fno-lookup-tables -fsave-optimization-record
#$(info $(CFLAGS))
AS := $(GCCPATH)arm-none-eabi-gcc
LD := $(GCCPATH)arm-none-eabi-gcc
#LDFLAGS += -O0 -gdwarf-2 -gstrict-dwarf
LDFLAGS += -O3 -Os
LDLIBS += -lm -lgcc -lc
# import rules to compile glyphs(/pone)
include $(BOLOS_SDK)/Makefile.glyphs
### variables processed by the common makefile.rules of the SDK to grab source files and include dirs
APP_SOURCE_PATH += src
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl
SDK_SOURCE_PATH += lib_ux
ifeq ($(TARGET_NAME),TARGET_NANOX)
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
DEFINES += HAVE_UX_LEGACY
endif
#########################
cformat:
clang-format -i src/*.c src/*.h
load: all
cp -a release/$(APPNAME).elf bin/app.elf
cp -a release/$(APPNAME).hex bin/app.hex
cp -a release/$(APPNAME).asm debug/app.asm
cp -a release/$(APPNAME).map debug/app.map
python -m ledgerblue.loadApp $(APP_LOAD_PARAMS)
run:
python -m ledgerblue.runApp --appName $(APPNAME)
exit:
echo -e "0020008206313233343536\n0002000000" |scriptor -r "Ledger Nano S [Nano S] (0001) 01 00"
delete:
python -m ledgerblue.deleteApp $(COMMON_DELETE_PARAMS)
# import generic rules from the user and SDK
-include Makefile.rules
#include $(BOLOS_SDK)/Makefile.rules
#add dependency on custom makefile filename
dep/%.d: %.c Makefile
# Import generic rules from the SDK
include $(BOLOS_SDK)/Makefile.standard_app

@ -1,39 +0,0 @@
#*******************************************************************************
# Ledger SDK
# (c) 2017 Ledger
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*******************************************************************************
# temporary redef, to ensure wider compliance of the SDK with pre-1.6 apps
ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOX TARGET_NANOS2))
SDK_SOURCE_PATH += lib_bagl
else
SDK_SOURCE_PATH += lib_bagl lib_ux
endif
# adding the correct target header to sources
SDK_SOURCE_PATH += target/$(TARGET)/include
# Expose all SDK header files with their full relative path to the SDK root folder
INCLUDES_PATH += ${BOLOS_SDK}
SOURCE_PATH += $(dir $(foreach libdir, $(APP_SOURCE_PATH), $(dir $(shell find $(libdir) -name '*.[csS]')))) $(BOLOS_SDK)/src $(foreach libdir, $(SDK_SOURCE_PATH), $(dir $(shell find $(BOLOS_SDK)/$(libdir) -name '*.[csS]')))
SOURCE_FILES := $(foreach path, $(SOURCE_PATH),$(shell find $(path) -name '*.[csS]') ) $(GLYPH_DESTC)
INCLUDES_PATH += $(dir $(foreach libdir, $(SDK_SOURCE_PATH), $(dir $(shell find $(BOLOS_SDK)/$(libdir) -name '*.h')))) include $(BOLOS_SDK)/include $(BOLOS_SDK)/include/arm $(dir $(shell find $(APP_SOURCE_PATH) -name '*.h')) $(GLYPH_SRC_DIR)
VPATH += $(dir $(SOURCE_FILES))
OBJECT_FILES += $(sort $(addprefix $(OBJ_DIR)/, $(addsuffix .o, $(basename $(notdir $(SOURCE_FILES))))))
DEPEND_FILES += $(sort $(addprefix $(DEP_DIR)/, $(addsuffix .d, $(basename $(notdir $(SOURCE_FILES))))))
include $(BOLOS_SDK)/Makefile.rules_generic

@ -1,59 +1,331 @@
## GnuPG application: blue-app-gnupg
# GnuPG application
GnuPG application for Nano S and Nano X
[![Ensure compliance with Ledger guidelines](https://github.com/LedgerHQ/app-openpgp/actions/workflows/guidelines_enforcer.yml/badge.svg)](https://github.com/LedgerHQ/app-openpgp/actions/workflows/guidelines_enforcer.yml)
This application implements "The OpenPGP card" specification revision 3.0. This specification is available in *doc* directory and at https://g10code.com/p-card.html .
The application supports:
- RSA with key up to 4096 bits
- ECDSA with secp256k1
- EDDSA with Ed25519 curve
- ECDH with secp256k1 and curve25519 curves
[![Build and run functional tests using ragger through reusable workflow](https://github.com/LedgerHQ/app-openpgp/actions/workflows/build_and_functional_tests.yml/badge.svg?branch=master)](https://github.com/LedgerHQ/app-openpgp/actions/workflows/build_and_functional_tests.yml)
GnuPG application for Ledger devices
This release has known missing parts (see also Add-on) :
This application implements "The OpenPGP card" specification revision 3.3.
This specification is available in *doc* directory and at <https://g10code.com/p-card.html>.
* Ledger Blue support
* Seed mode ON/OFF via apdu
The application supports:
- RSA with key up to 3072 bits
- ECDSA with secp256R1 and secp256K1
- EDDSA with Ed25519 curve
- ECDH with secp256R1, secp256K1 and curve25519 curves
## Installation and Usage
See the full doc at https://github.com/LedgerHQ/blue-app-openpgp-card/blob/master/doc/user/blue-app-openpgp-card.pdf
See the full doc in [rst](doc/user/app-openpgp.rst), or in [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/user/app-openpgp.pdf>)
## Add-on
The GnuPG application implements the following addon:
- serial modification
- on screen reset
- 3 independent key slots
- seeded key generation
Technical specification is available at https://github.com/LedgerHQ/blue-app-openpgp-card/blob/master/doc/developper/gpgcard3.0-addon.rst
- Serial modification
- On screen reset
- 3 independent key slots (except for Nanos, where we have only a single slot)
- Seeded key generation
Technical specification is available in [rst](doc/developer/gpgcard-addon.rst), or in [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/developer/gpgcard-addon.pdf>)
### Key slot
"The OpenPGP card" specification specifies:
- 3 asymmetric keys : Signature, Decryption, Authentication
- 1 symmetric key
The OpenPGP card specification indicates:
- 3 asymmetric keys:
- Signature,
- Decryption
- Authentication
- 1 symmetric key
The blue application allow you to store 3 different key sets, named slot. Each slot contains the above 4 keys.
The application allows you to store 3 different key sets, named slot. Each slot contains the above 4 keys.
You can choose the active slot on the main screen.
When installed the default slot is "1". You can change it in settings.
### seeded key generation
### Seeded key generation
A seeded mode is implemented in order to restore private keys on a new token.
In this mode key material is generated from the global token seeded.
In this mode, key material is generated from the global token seed.
Please consider SEED mode as experimental.
Also, a backup/restore mechanism is provided. Please report to the [Documentation](#documentation).
More details to come...
> Warning: Without such configuration, an OS or App update will cause your private key to be lost!"
The following is a repeatable process that will generate the same keys and fingerprints
(even with different card serial numbers).
1. Reset the OpenPGP App
1. Set Key Templates from the OpenPGP App menu, if needed
1. On computer, use `gpg` to edit the key with a fixed timestamp:
```shell
gpg --faked-system-time 19990101T000000! --card-edit # note the exclamation mark to keep the time fixed
gpg> admin
gpg> generate
... # complete the wizard
```
While doing this, ensure that you use the same `--faked-system-time` and "Real Name" and "Email"
during the generation wizard to reproduce the same keys each time.
### On screen reset
The application can be reset as if it was fresh installed. In settings, choose reset and confirm.
## Quick start guide
### With VSCode
You can quickly setup a convenient environment to build and test your application by using
[Ledger's VSCode developer tools extension](https://marketplace.visualstudio.com/items?itemName=LedgerHQ.ledger-dev-tools)
which leverages the [ledger-app-dev-tools](https://github.com/LedgerHQ/ledger-app-builder/pkgs/container/ledger-app-builder%2Fledger-app-dev-tools)
docker image.
It will allow you, whether you are developing on macOS, Windows or Linux,
to quickly **build** your apps, **test** them on **Speculos** and **load** them on any supported device.
- Install and run [Docker](https://www.docker.com/products/docker-desktop/).
- Make sure you have an X11 server running:
- On Ubuntu Linux, it should be running by default.
- On macOS, install and launch [XQuartz](https://www.xquartz.org/)
(make sure to go to XQuartz > Preferences > Security and check "Allow client connections").
- On Windows, install and launch [VcXsrv](https://sourceforge.net/projects/vcxsrv/)
(make sure to configure it to disable access control).
- Install [VScode](https://code.visualstudio.com/download) and add [Ledger's extension](https://marketplace.visualstudio.com/items?itemName=LedgerHQ.ledger-dev-tools).
- Open a terminal and clone `app-openpgp` with `git clone git@github.com:LedgerHQ/app-openpgp.git`.
- Open the `app-openpgp` folder with VSCode.
- Use Ledger extension's sidebar menu or open the tasks menu with `ctrl + shift + b`
(`command + shift + b` on a Mac) to conveniently execute actions:
- Build the app for the device model of your choice with `Build`.
- Test your binary on [Speculos](https://github.com/LedgerHQ/speculos) with `Run with Speculos`.
- You can also run functional tests, load the app on a physical device, and more.
> The terminal tab of VSCode will show you what commands the extension runs behind the scene.
### With a terminal
The [ledger-app-dev-tools](https://github.com/LedgerHQ/ledger-app-builder/pkgs/container/ledger-app-builder%2Fledger-app-dev-tools)
docker image contains all the required tools and libraries to **build**, **test** and **load** an application.
You can download it from the ghcr.io docker repository:
```shell
sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
```
You can then enter this development environment by executing the following command
from the directory of the application `git` repository:
#### Linux (Ubuntu)
```shell
sudo docker run --rm -ti --user "$(id -u):$(id -g)" --privileged -v "/dev/bus/usb:/dev/bus/usb" -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
```
#### macOS
```shell
sudo docker run --rm -ti --user "$(id -u):$(id -g)" --privileged -v "$(pwd -P):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
```
#### Windows (with PowerShell)
```shell
docker run --rm -ti --privileged -v "$(Get-Location):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
```
The application's code will be available from inside the docker container,
you can proceed to the following compilation steps to build your app.
## Compilation and load
To easily setup a development environment for compilation and loading on a physical device, you can use the [VSCode integration](#with-vscode)
whether you are on Linux, macOS or Windows.
If you prefer using a terminal to perform the steps manually, you can use the guide below.
### Compilation
Setup a compilation environment by following the [shell with docker approach](#with-a-terminal).
From inside the container, use the following command to build the app:
```shell
make DEBUG=1 # compile optionally with PRINTF
```
You can choose which device to compile and load for by setting the `BOLOS_SDK` environment variable to the following values:
- `BOLOS_SDK=$NANOS_SDK`
- `BOLOS_SDK=$NANOX_SDK`
- `BOLOS_SDK=$NANOSP_SDK`
- `BOLOS_SDK=$STAX_SDK`
### Loading on a physical device
This step will vary slightly depending on your platform.
> Your physical device must be connected, unlocked and the screen showing the dashboard (not inside an application).
#### Linux (Ubuntu)
First make sure you have the proper udev rules added on your host.
See [udev-rules](https://github.com/LedgerHQ/udev-rules)
Then once you have [opened a terminal](#with-a-terminal) in the `app-builder` image and [built the app](#compilation-and-load)
for the device you want, run the following command:
```shell
# Run this command from the app-builder container terminal.
make load # load the app on a Nano S by default
```
[Setting the BOLOS_SDK environment variable](#compilation-and-load) will allow you to load
on whichever supported device you want.
#### macOS / Windows (with PowerShell)
> It is assumed you have [Python](https://www.python.org/downloads/) installed on your computer.
Run these commands on your host from the app's source folder once you have [built the app](#compilation-and-load)
for the device you want:
```shell
# Install Python virtualenv
python3 -m pip install virtualenv
# Create the 'ledger' virtualenv
python3 -m virtualenv ledger
```
Enter the Python virtual environment
- macOS: `source ledger/bin/activate`
- Windows: `.\ledger\Scripts\Activate.ps1`
```shell
# Install Ledgerblue (tool to load the app)
python3 -m pip install ledgerblue
# Load the app.
python3 -m ledgerblue.runScript --scp --fileName bin/app.apdu --elfFile bin/app.elf
```
## Tests
The OpenPGP app comes with different tests:
- Functional Tests implemented with Ledger's [Ragger](https://github.com/LedgerHQ/ragger) test framework.
- Unit Tests, allowing to test basic simple functions
- Manual Tests, using some script to perform real tests on the device:
- Key generation
- Encryption/Decryption
- Sign/Verify
### Functional Tests (Ragger based)
#### Linux (Ubuntu)
On Linux, you can use [Ledger's VS Code extension](#with-vscode) to run the tests.
If you prefer not to, open a terminal and follow the steps below.
Install the tests requirements:
```shell
pip install -r tests/requirements.txt
```
Then you can:
Run the functional tests (here for nanos but available for any device once you have built the binaries):
```shell
pytest tests/ --tb=short -v --device nanos
```
Or run your app directly with Speculos
```shell
speculos --model nanos build/nanos/bin/app.elf
```
#### macOS / Windows
To test your app on macOS or Windows, it is recommended to use [Ledger's VS Code extension](#with-vscode)
to quickly setup a working test environment.
You can use the following sequence of tasks and commands (all accessible in the **extension sidebar menu**):
- `Select build target`
- `Build app`
Then you can choose to execute the functional tests:
- Use `Run tests`.
Or simply run the app on the Speculos emulator:
- `Run with Speculos`.
### Unit Tests
Those tests are available in the directory `unit-tests`. Please see the corresponding [README](unit-tests/README.md)
to compile and run them.
### Manual Tests
Those tests are available in the directory `manual-tests`. This consists in a helper script (`manual.sh`)
corresponding to different cases described in the document [Quick tests](doc/developer/quick-test.md).
## Documentation
Several documents are available.
Functional Specification of the OpenPGP Application available [here](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/specification/OpenPGP-smart-card-application.pdf>),
but also at <https://g10code.com/p-card.html>.
User documentation for the Ledger Application available here: [rst](doc/user/app-openpgp.rst),
or [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/user/app-openpgp.pdf>)
Developer documentation related to the Ledger Add-ons available here: [rst](doc/developer/gpgcard-addon.rst),
or [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/developer/gpgcard-addon.pdf>)
A Quick Test document to perform `Manual Tests` available [here](doc/developer/quick-test.md)
The pdf documentation for **User** and **Developer** are available, and can be generated from
the corresponding `.rst` files (in the same respective directories) using this command:
```shell
cd doc/
./generate.sh
```
## Continuous Integration
The flow processed in [GitHub Actions](https://github.com/features/actions) is the following:
- Ledger guidelines enforcer which verifies that an app is compliant with Ledger guidelines.
The successful completion of this reusable workflow is a mandatory step for an app to be available
on the Ledger application store. More information on the guidelines can be found
in the repository [ledger-app-workflow](https://github.com/LedgerHQ/ledger-app-workflows)
- Code formatting with [clang-format](http://clang.llvm.org/docs/ClangFormat.html)
- Compilation of the application for all Ledger hardware in [ledger-app-builder](https://github.com/LedgerHQ/ledger-app-builder)
- Unit tests of C functions with [cmocka](https://cmocka.org/) (see [unit-tests/](unit-tests/))
- End-to-end tests with [Speculos](https://github.com/LedgerHQ/speculos) emulator
and [ragger](https://github.com/LedgerHQ/ragger) (see [tests/](tests/))
- Code coverage with [gcov](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html)/[lcov](http://ltp.sourceforge.net/coverage/lcov.php)
and upload to [codecov.io](https://about.codecov.io)
It outputs 3 artifacts:
- `compiled_app_binaries` within binary files of the build process for each device
- `code-coverage` within HTML details of code coverage
## Known limitations
Today, the current App has some known limitations.
- RSA4096 is disabled, because of an issue with the watchdog, resetting the device
during long prime number operation.
- Using Ed25519 template, the decrypt doesn't output a correct result.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

@ -0,0 +1,344 @@
..
Ledger App OpenPGP.
(c) 2024 Ledger SAS.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
..
------------------------------------------------------------------------
LaTex substitution Definition
------------------------------------------------------------------------
License
=======
| Ledger App OpenPGP.
| (c) 2024 Ledger SAS.
|
| Licensed under the Apache License, Version 2.0 (the "License");
| you may not use this file except in compliance with the License.
| You may obtain a copy of the License at
|
| http://www.apache.org/licenses/LICENSE-2.0
|
| Unless required by applicable law or agreed to in writing, software
| distributed under the License is distributed on an "AS IS" BASIS,
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
| See the License for the specific language governing permissions and
| limitations under the License.
Introduction
============
OpenPGP Card Application add-ons summary
----------------------------------------
Key management:
~~~~~~~~~~~~~~~
OpenPGP Application manage 4 keys to Perform Security Operation (PSO) plus 2 for secure channel.
The 4 keys are defined as follow:
- One asymmetric signature private key (RSA or EC), named 'sig'
- One asymmetric decryption private key (RSA or EC), named 'dec'
- One asymmetric authentication private key (RSA or EC), named 'aut'
- One symmetric decryption private key (AES), named 'sym0'
The 3 first asymmetric keys can be either randomly generated on-card or
explicitly imported from outside.
The 4th is imported from outside.
It's never possible to retrieve private key from the card.
This add-on specification propose a solution to derive those keys from the
master seed managed by the Ledger Token.
This allows owner to restore a broken token without the needs to keep track of keys
outside the card.
Moreover this add-on specification propose to manage multiple set of the 4 previously described keys.
Keys Slots
~~~~~~~~~~
To modify the keys slot, just select the corresponding menu from the screen.
.. image:: slots.png
Random number generation
~~~~~~~~~~~~~~~~~~~~~~~~
OpenPGP Application provides, as optional feature, to generate random bytes.
This add-on specification propose new type of random generation:
- random prime number generation
- seeded random number
- seeded prime number generation
Key Backup
~~~~~~~~~~
A full key backup mechanism is provided.
Ledger OpenPGP Application
==========================
How
---
Deterministic key derivation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The deterministic key derivation process relies on the **BIP32** scheme.
The master install path of the App is set to ``/0x80'GPG'``, aka ``/80475047``.
Deterministic key derivation maybe activated in:
| ``Settings -> Seed Mode -> Set on``
This activation remains effective until *set off* is selected.
The key management remains the same if seed mode is *on* or *off*. So there is no performance impact when using seeded keys.
Seeded keys are generated as follow:
**Step 1**:
For a given keys slot n, starting from 1, a seed is first derived with the following path
| ``Sn = BIP32_derive (/0x80475047/n)``
**Step 2**:
Then specific seeds are derived with the *SHA3-XOF* function for each of the 4 key:
| ``Sk[i] = SHA3-XOF(SHA256(Sn \| <key_name> \| int16(i)), length)``
Where:
- Sn is the dedicated slot seed from step 1.
- key_name is one of 'sig ','dec ', 'aut ', 'sym0', each 4 characters.
- i is the index, starting from 1, of the desired seed (see below)
**Step 3**:
*RSA key generation*
Generate two seed Sp, Sq in step2 with:
- i € {1,2}
- length equals to half key size
Generate two prime numbers p, q:
- p = next_prime(Sp)
- q = next_prime(Sq)
Generate RSA key pair as usual:
- choose e
- n = p*q
- d = inv(e) mod (p-1)(q-1)
*ECC key generation*
Generate one seed Sd in step2 with:
- i = 1
- length equals to curve size
Generate ECC key pair:
- d = Sd
- W = d.G
*AES key generation*
Generate one seed Sd in step2 with:
- i = 1
- length equals to 16
Generate AES key:
- k = Sk
Deterministic random number
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The deterministic random number generation relies on the **BIP32** scheme.
The master install path of the App is set to ``/0x80'GPG'``, aka ``/80475047``.
**Random prime number generation**:
For a given length *L*:
- generate random number r of *L* bytes.
- generate rp = next_prime(r)
- return rp
**Seeded random number**:
For a given length *L* and seed *S*:
- generate Sr = BIP32_derive(/0x80475047/0x0F0F0F0F)
- generate r = SHA3-XOF(SHA256(Sr \| 'rnd' \| S), L)
- return r
**Seeded prime number generation**:
For a given length *L* and seed *S*:
- generate r as for "Seeded random number"
- generate rp = next_prime(r)
- return rp
Key Backup & Restore
~~~~~~~~~~~~~~~~~~~~
In order to backup/restore private key the commands `put_data` and `get_data` accept the tags:
- `B6` (signature key)
- `B8` (encryption key)
- `A4` (authentication).
`put_data` command accept the exact output of `get_data`. The `get_data` command
return both the public and private key.
For security and confidentiality, private key is returned encrypted in AES.
The key used is derived according to previously described AES key derivation
with name 'key'.
The data payload is formatted as follow:
+-------+--------------------------+
| size | Description |
+=======+==========================+
| 4 | OS Target ID |
+-------+--------------------------+
| 4 | API Level |
+-------+--------------------------+
| 4 | compliance Level |
+-------+--------------------------+
| 4 | public key size |
+-------+--------------------------+
| var | public key |
+-------+--------------------------+
| 4 | private key size |
+-------+--------------------------+
| var | encrypted private key |
+-------+--------------------------+
APDU Modification
-----------------
Key Slot management
~~~~~~~~~~~~~~~~~~~~
Key slots are managed by data object *01F1* and *01F2* witch are
manageable by PUT/GET DATA command as for others DO and organized as follow.
On application reset, the *01F2* content is set to *Default Slot* value
of *01F1*.
*01F1:*
+-------+----------------------------------+-------+
| bytes | Description | R/W |
+=======+==================================+=======+
| 1 | Number of slot | R |
+-------+----------------------------------+-------+
| 2 | Default slot | R/W |
+-------+----------------------------------+-------+
| 3 | Allowed slot selection method | R/W |
+-------+----------------------------------+-------+
Byte 3 is endoced as follow:
+----+----+----+----+----+----+----+----+-------------------------+
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | \- | x | selection by APDU |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | x | \- | selection by screen |
+----+----+----+----+----+----+----+----+-------------------------+
*01F2:*
+--------+-----------------+-------+
| bytes | Description | R/W |
+========+=================+=======+
| 1 | Current slot | R/W |
+--------+-----------------+-------+
*01F0:*
+--------+-----------------+-------+
| bytes | Description | R/W |
+========+=================+=======+
| 1-3 | 01F1 content | R |
+--------+-----------------+-------+
| 4 | 01F2 content | R |
+--------+-----------------+-------+
*Access Conditions:*
+------+-----------+------------+
| DO | Read | Write |
+======+===========+============+
| 01F0 | Always | Never |
+------+-----------+------------+
| 01F1 | Always | Verify PW3 |
+------+-----------+------------+
| 01F2 | Always | Verify PW2 |
+------+-----------+------------+
Deterministic key derivation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
P2 parameter of GENERATE ASYMMETRIC KEY PAIR is set to (hex value):
- 00 for true random key generation
- 01 for seeded random key
Deterministic random number
~~~~~~~~~~~~~~~~~~~~~~~~~~~
P1 parameter of GET CHALLENGE is a bit-field encoded as follow:
+----+----+----+----+----+----+----+----+-------------------------+
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | \- | x | seeded random |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | x | \- | prime random |
+----+----+----+----+----+----+----+----+-------------------------+
When *seeded mode* is set, data field contains the seed and P2 contains
the length of random bytes to generate.
Other minor add-on
------------------
GnuPG use both fingerprints and serial number to identify key on card.
So, the `put_data` command is able to modify the AID file with '4F' tag.
In that case the data field shall be 4 bytes length and shall contain
the new serial number. '4F' is protected by PW3 (admin) PIN.

@ -0,0 +1,361 @@
# Quick Test guide
This page helps the developer to quickly test basic features of the OpenPGP Application.
Please note this guideline is targeting Linux (Ubuntu) only machine, but once installed,
all commands are valid on any other OS.
## Step 1: Install your device
Do a fresh installation of OpenPGP Application on the device.
## Step 2: Setup conf
### Tools and scripts
Ensure `pgp` is available on your host machine.
Install the needed mandatory tools:
```shell
sudo apt install libpcsclite-dev scdaemon pcscd -y
```
Optionally, the following diagnostic tool can also be installed:
```shell
sudo apt install pcsc-tools -y
```
You can check the tools are operational with the commands (see *help* for other options):
```shell
$ pcsc_scan -c
Thu Jan 18 09:45:19 2024
Reader 0: Ledger Nano S Plus [Nano S Plus] (0001) 00 00
Event number: 0
Card state: Card inserted,
ATR: 3B 00
Reader 1: Alcor Micro AU9540 01 00
Event number: 0
Card state: Card removed,
$ p11-kit list-modules
p11-kit-trust: p11-kit-trust.so
library-description: PKCS#11 Kit Trust Module
library-manufacturer: PKCS#11 Kit
library-version: 0.24
token: System Trust
manufacturer: PKCS#11 Kit
model: p11-kit-trust
serial-number: 1
hardware-version: 0.24
flags:
write-protected
token-initialized
opensc-pkcs11: opensc-pkcs11.so
library-description: OpenSC smartcard framework
library-manufacturer: OpenSC Project
library-version: 0.22
token: OpenPGP card (User PIN)
manufacturer: OpenPGP project
model: PKCS#15 emulated
serial-number: 2c97c3e750db
hardware-version: 3.3
firmware-version: 3.3
flags:
rng
login-required
user-pin-initialized
protected-authentication-path
token-initialized
token: OpenPGP card (User PIN (sig))
manufacturer: OpenPGP project
model: PKCS#15 emulated
serial-number: 2c97c3e750db
hardware-version: 3.3
firmware-version: 3.3
flags:
rng
login-required
user-pin-initialized
protected-authentication-path
token-initialized
Check the installation of CCID driver and more particularly its device config:
Edit the file `/etc/libccid_Info.plist`, and check if the Ledger devices are correctly defined.
Please take care, the different lists are ordered in the same way. Do not insert elements anywhere!
> Note: To add a new Ledger device, check [doc/user/app-openpgp.rst](../user/app-openpgp.rst)
### Manual Tests
Jump into the directory `manual-tests`. There is a helper script to perform some of the following described operations.
```shell
cd manual-tests/
$ ./manual.sh -h
Usage: ./manual.sh <options>
Options:
-c <init|card|encrypt|decryptsign|verify> : Requested command
-v : Verbose mode
-h : Displays this help
```
The `init` command allows to prepare a local `gnupg` home directory, with the default minimal config file for *scdaemon*.
For further investigations, you can also add in the file `manual-tests/gnupg/scdaemon.conf` the following lines:
```shell
debug-level expert
debug 11
log-file /tmp/scdaemon.log
```
## Step 3: Verify the card status and the pin code
Launch the Application on the device.
Now, on the PC, inside a terminal:
```shell
$ killall scdaemon gpg-agent
$ gpg --homedir $(pwd)/gnupg --card-edit
gpg: keybox 'xxxx/manual-tests/gnupg/pubring.kbx' created
Reader ...........: Ledger Nano S Plus [Nano S Plus] (0001) 01 00
Application ID ...: D2760001240103032C97B7DA92860000
Version ..........: 3.3
Manufacturer .....: unknown
Serial number ....: B7DA9286
Name of cardholder: [not set]
Language prefs ...: [not set]
Salutation .......:
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
```
This gives the status of the current card. We can see on the 1st line, the detected Card Reader (i.e. the Ledger device).
In the Application ID information, we can see, concatenated:
- `D27600012401`: The *Registered application provider identifier* (ISO 7816-5)
- `01`: The *Proprietary application identifier extension* (OpenPGP)
- `0303`: The current specification version *3.3*
- `2C97`: The Ledger Manufacturer Id
- `B7DA9286`: the Card Serial
And currently, no keys are present on the Card.
Now, we can verify the Card pin code using the `verify` command.
You will be prompt to validate with the buttons, or enter the pin code, depending on the Application settings.
```shell
gpg/card> verify
Reader ...........: Ledger Nano S Plus [Nano S Plus] (0001) 01 00
Application ID ...: D2760001240103032C97B7DA92860000
Version ..........: 3.3
Manufacturer .....: unknown
Serial number ....: B7DA9286
Name of cardholder: [not set]
Language prefs ...: [not set]
Salutation .......:
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
```
> Note: you can exit by using the command `quit` or using `CTRL-D`.
## Step 4: Change to screen pin style
Then on the device, go to:
```text
settings -> PIN mode, and select On Screen
settings -> PIN mode, and select Set as default
```
unplug and replug the nanos, relaunch the Application, and check:
```text
settings -> PIN mode, you should have On Screen # + (DASH and PLUS)
```
## Step 5: Create RSA keys
Back in the terminal window, in `manual-tests` directory.
> Note: During this phase PIN has to be validate on the device.
```shell
$ killall scdaemon gpg-agent
$ gpg --homedir $(pwd)/gnupg --card-edit
gpg: keybox 'xxxx/manual-tests/gnupg/pubring.kbx' created
Reader ...........: Ledger Nano S Plus [Nano S Plus] (0001) 01 00
Application ID ...: D2760001240103032C97B7DA92860000
Version ..........: 3.3
Manufacturer .....: unknown
Serial number ....: B7DA9286
Name of cardholder: [not set]
Language prefs ...: [not set]
Salutation .......:
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
```
Now, switch to **Admin** mode, and generate the keys (this is an interactive operation).
```shell
gpg/card> admin
Admin commands are allowed
gpg/card> generate
Make off-card backup of encryption key? (Y/n) n
Please note that the factory settings of the PINs are
PIN = '123456' Admin PIN = '12345678'
You should change them using the command --change-pin
What keysize do you want for the Signature key? (2048) 2048
What keysize do you want for the Encryption key? (2048) 2048
What keysize do you want for the Authentication key? (2048) 2048
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: testkey
Email address:
Comment:
You selected this USER-ID:
"testkey"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
gpg: xxxx/manual-tests/gnupg/trustdb.gpg: trustdb created
gpg: key 5ED17DF289C757A2 marked as ultimately trusted
gpg: directory 'xxxx/manual-tests/gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as 'xxxx/manual-tests/gnupg/openpgp-revocs.d/7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2.rev'
public and secret key created and signed.
gpg/card> quit
pub rsa2048 2017-10-03 [SC]
7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2
uid testkey
sub rsa2048 2017-10-03 [A]
sub rsa2047 2017-10-03 [E]
```
## Step 6: Encrypt/Decrypt
Simple *Encrypt* and *Decrypt* test to use and check the generated keys.
Start to create a dummy file to be encrypted and checked.
```shell
$ killall scdaemon gpg-agent
$ echo CLEAR > foo.txt
```
### Encrypt
Use this command to encrypt. Please note we specify the key to be used.
```shell
$ gpg --homedir $(pwd)/gnupg --encrypt --recipient testkey foo.txt
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
```
Just kill the processes to force pin to be asked...
```shell
$ killall gpg-agent scdaemon
```
### Decrypt
Use this command to encrypt. Here, no need to specify the key...
```shell
$ gpg --homedir $(pwd)/gnupg --decrypt foo.txt.gpg > foo_dec.txt
gpg: encrypted with 2047-bit RSA key, ID 602FE5EB7BFA4B00, created 2017-10-03
"testkey"
$ cat foo_dec.txt
CLEAR
```
## Step 7: Sign/Verify
Simple *Sign* and *Verify* test to use and check signature with the generated keys.
Start to create a dummy file to be signed and verified.
```shell
$ killall scdaemon gpg-agent
$ echo CLEAR > foo.txt
```
### Sign
Use this command to sign. The generated file is the encrypted signature.
```shell
$ gpg --homedir $(pwd)/gnupg --sign foo.txt
```
Just kill the processes to force pin to be asked...
```shell
$ killall gpg-agent scdaemon
```
### Verify
Use this command to verify the signature
```shell
$ gpg --homedir $(pwd)/gnupg --verify foo.txt.gpg
gpg: Signature made jeu. 18 janv. 2024 09:59:33 CET
gpg: using RSA key FD2D7E0C99825F8515EDB544156DEEF5959D4DC6
gpg: Good signature from "testkey" [ultimate]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

@ -0,0 +1,516 @@
% Options for packages loaded elsewhere
\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref}
\PassOptionsToPackage{hyphens}{url}
$if(colorlinks)$
\PassOptionsToPackage{dvipsnames,svgnames*,x11names*}{xcolor}
$endif$
$if(dir)$
$if(latex-dir-rtl)$
\PassOptionsToPackage{RTLdocument}{bidi}
$endif$
$endif$
$if(CJKmainfont)$
\PassOptionsToPackage{space}{xeCJK}
$endif$
%
\documentclass[
$if(fontsize)$
$fontsize$,
$endif$
$if(lang)$
$babel-lang$,
$endif$
$if(papersize)$
$papersize$paper,
$endif$
$if(beamer)$
ignorenonframetext,
$if(handout)$
handout,
$endif$
$if(aspectratio)$
aspectratio=$aspectratio$,
$endif$
$endif$
$for(classoption)$
$classoption$$sep$,
$endfor$
]{$documentclass$}
$if(beamer)$
$if(background-image)$
\usebackgroundtemplate{%
\includegraphics[width=\paperwidth]{$background-image$}%
}
$endif$
\usepackage{pgfpages}
\setbeamertemplate{caption}[numbered]
\setbeamertemplate{caption label separator}{: }
\setbeamercolor{caption name}{fg=normal text.fg}
\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$
$for(beameroption)$
\setbeameroption{$beameroption$}
$endfor$
% Prevent slide breaks in the middle of a paragraph
\widowpenalties 1 10000
\raggedbottom
$if(section-titles)$
\setbeamertemplate{part page}{
\centering
\begin{beamercolorbox}[sep=16pt,center]{part title}
\usebeamerfont{part title}\insertpart\par
\end{beamercolorbox}
}
\setbeamertemplate{section page}{
\centering
\begin{beamercolorbox}[sep=12pt,center]{part title}
\usebeamerfont{section title}\insertsection\par
\end{beamercolorbox}
}
\setbeamertemplate{subsection page}{
\centering
\begin{beamercolorbox}[sep=8pt,center]{part title}
\usebeamerfont{subsection title}\insertsubsection\par
\end{beamercolorbox}
}
\AtBeginPart{
\frame{\partpage}
}
\AtBeginSection{
\ifbibliography
\else
\frame{\sectionpage}
\fi
}
\AtBeginSubsection{
\frame{\subsectionpage}
}
$endif$
$endif$
$if(beamerarticle)$
\usepackage{beamerarticle} % needs to be loaded first
$endif$
$if(fontfamily)$
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
$else$
\usepackage{lmodern}
$endif$
$if(linestretch)$
\usepackage{setspace}
$endif$
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{textcomp} % provide euro and other symbols
\else % if luatex or xetex
$if(mathspec)$
\ifxetex
\usepackage{mathspec}
\else
\usepackage{unicode-math}
\fi
$else$
\usepackage{unicode-math}
$endif$
\defaultfontfeatures{Scale=MatchLowercase}
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
$if(mainfont)$
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
$endif$
$if(sansfont)$
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
$endif$
$if(monofont)$
\setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$}
$endif$
$for(fontfamilies)$
\newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$}
$endfor$
$if(mathfont)$
$if(mathspec)$
\ifxetex
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
\else
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
\fi
$else$
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
$endif$
$endif$
$if(CJKmainfont)$
\ifxetex
\usepackage{xeCJK}
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
\fi
$endif$
$if(luatexjapresetoptions)$
\ifluatex
\usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset}
\fi
$endif$
$if(CJKmainfont)$
\ifluatex
\usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec}
\setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
\fi
$endif$
\fi
$if(beamer)$
$if(theme)$
\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$}
$endif$
$if(colortheme)$
\usecolortheme{$colortheme$}
$endif$
$if(fonttheme)$
\usefonttheme{$fonttheme$}
$endif$
$if(mainfont)$
\usefonttheme{serif} % use mainfont rather than sansfont for slide text
$endif$
$if(innertheme)$
\useinnertheme{$innertheme$}
$endif$
$if(outertheme)$
\useoutertheme{$outertheme$}
$endif$
$endif$
% Use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
\IfFileExists{microtype.sty}{% use microtype if available
\usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
$if(indent)$
$else$
\makeatletter
\@ifundefined{KOMAClassName}{% if non-KOMA class
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
}{% if KOMA class
\KOMAoptions{parskip=half}}
\makeatother
$endif$
$if(verbatim-in-note)$
\usepackage{fancyvrb}
$endif$
\usepackage{xcolor}
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
\hypersetup{
$if(title-meta)$
pdftitle={$title-meta$},
$endif$
$if(author-meta)$
pdfauthor={$author-meta$},
$endif$
$if(lang)$
pdflang={$lang$},
$endif$
$if(subject)$
pdfsubject={$subject$},
$endif$
$if(keywords)$
pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$},
$endif$
$if(colorlinks)$
colorlinks=true,
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
filecolor=$if(filecolor)$$filecolor$$else$Maroon$endif$,
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
$else$
hidelinks,
$endif$
pdfcreator={LaTeX via pandoc}}
\urlstyle{same} % disable monospaced font for URLs
$if(verbatim-in-note)$
\VerbatimFootnotes % allow verbatim text in footnotes
$endif$
$if(geometry)$
$if(beamer)$
\geometry{$for(geometry)$$geometry$$sep$,$endfor$}
$else$
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
$endif$
$endif$
$if(beamer)$
\newif\ifbibliography
$endif$
$if(listings)$
\usepackage{listings}
\newcommand{\passthrough}[1]{#1}
\lstset{defaultdialect=[5.3]Lua}
\lstset{defaultdialect=[x86masm]Assembler}
$endif$
$if(lhs)$
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
$endif$
$if(highlighting-macros)$
$highlighting-macros$
$endif$
$if(tables)$
\usepackage{longtable,booktabs}
$if(beamer)$
\usepackage{caption}
% Make caption package work with longtable
\makeatletter
\def\fnum@table{\tablename~\thetable}
\makeatother
$else$
% Correct order of tables after \paragraph or \subparagraph
\usepackage{etoolbox}
\makeatletter
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
\makeatother
% Allow footnotes in longtable head/foot
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
\makesavenoteenv{longtable}
$endif$
$endif$
$if(graphics)$
\usepackage{graphicx}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
% Set default figure placement to htbp
\makeatletter
\def\fps@figure{htbp}
\makeatother
$endif$
$if(links-as-notes)$
% Make links footnotes instead of hotlinks:
\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}}
$endif$
$if(strikeout)$
\usepackage[normalem]{ulem}
% Avoid problems with \sout in headers with hyperref
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
$endif$
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
$if(numbersections)$
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
$else$
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
$endif$
$if(beamer)$
$else$
$if(block-headings)$
% Make \paragraph and \subparagraph free-standing
\ifx\paragraph\undefined\else
\let\oldparagraph\paragraph
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
\fi
\ifx\subparagraph\undefined\else
\let\oldsubparagraph\subparagraph
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
\fi
$endif$
$endif$
$if(pagestyle)$
\pagestyle{$pagestyle$}
$endif$
$for(header-includes)$
$header-includes$
$endfor$
$if(lang)$
\ifxetex
% Load polyglossia as late as possible: uses bidi with RTL languages (e.g. Hebrew, Arabic)
\usepackage{polyglossia}
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
$for(polyglossia-otherlangs)$
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
$endfor$
\else
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
$if(babel-newcommands)$
$babel-newcommands$
$endif$
\fi
$endif$
$if(dir)$
\ifxetex
% Load bidi as late as possible as it modifies e.g. graphicx
\usepackage{bidi}
\fi
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\TeXXeTstate=1
\newcommand{\RL}[1]{\beginR #1\endR}
\newcommand{\LR}[1]{\beginL #1\endL}
\newenvironment{RTL}{\beginR}{\endR}
\newenvironment{LTR}{\beginL}{\endL}
\fi
$endif$
$if(natbib)$
\usepackage[$natbiboptions$]{natbib}
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
$endif$
$if(biblatex)$
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
$for(bibliography)$
\addbibresource{$bibliography$}
$endfor$
$endif$
$if(csl-refs)$
\newlength{\cslhangindent}
\setlength{\cslhangindent}{1.5em}
\newenvironment{cslreferences}%
{$if(csl-hanging-indent)$\setlength{\parindent}{0pt}%
\everypar{\setlength{\hangindent}{\cslhangindent}}\ignorespaces$endif$}%
{\par}
$endif$
$if(title)$
\title{$title$$if(thanks)$\thanks{$thanks$}$endif$}
$endif$
$if(subtitle)$
$if(beamer)$
$else$
\usepackage{etoolbox}
\makeatletter
\providecommand{\subtitle}[1]{% add subtitle to \maketitle
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
}
\makeatother
$endif$
\subtitle{$subtitle$}
$endif$
\author{$for(author)$$author$$sep$ \and $endfor$}
\date{$date$}
$if(beamer)$
$if(institute)$
\institute{$for(institute)$$institute$$sep$ \and $endfor$}
$endif$
$if(titlegraphic)$
\titlegraphic{\includegraphics{$titlegraphic$}}
$endif$
$if(logo)$
\logo{\includegraphics{$logo$}}
$endif$
$endif$
\begin{document}
\begin{titlepage}
\centering
{\scshape\Huge OpenPGP Card Application \par}
{\scshape \huge Add-on \par}
\vspace{1cm}
{\scshape\LARGE Ledger SAS \par}
\vspace{2cm}
\includegraphics{../LogoLedger.png}
\vspace{1cm}
{\Large\itshape\url {https://github.com/LedgerHQ/app-openpgp}\par}
\vfill
{\large \today\par}
\end{titlepage}
$if(has-frontmatter)$
\frontmatter
$endif$
$if(title)$
$if(beamer)$
\frame{\titlepage}
$else$
\maketitle
$endif$
$if(abstract)$
\begin{abstract}
$abstract$
\end{abstract}
$endif$
$endif$
$for(include-before)$
$include-before$
$endfor$
$if(toc)$
$if(toc-title)$
\renewcommand*\contentsname{$toc-title$}
$endif$
$if(beamer)$
\begin{frame}[allowframebreaks]
$if(toc-title)$
\frametitle{$toc-title$}
$endif$
\tableofcontents[hideallsubsections]
\end{frame}
$else$
{
$if(colorlinks)$
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$}
$endif$
\setcounter{tocdepth}{$toc-depth$}
\tableofcontents
}
$endif$
$endif$
$if(lot)$
\listoftables
$endif$
$if(lof)$
\listoffigures
$endif$
$if(linestretch)$
\setstretch{$linestretch$}
$endif$
$if(has-frontmatter)$
\mainmatter
$endif$
$body$
$if(has-frontmatter)$
\backmatter
$endif$
$if(natbib)$
$if(bibliography)$
$if(biblio-title)$
$if(has-chapters)$
\renewcommand\bibname{$biblio-title$}
$else$
\renewcommand\refname{$biblio-title$}
$endif$
$endif$
$if(beamer)$
\begin{frame}[allowframebreaks]{$biblio-title$}
\bibliographytrue
$endif$
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
$if(beamer)$
\end{frame}
$endif$
$endif$
$endif$
$if(biblatex)$
$if(beamer)$
\begin{frame}[allowframebreaks]{$biblio-title$}
\bibliographytrue
\printbibliography[heading=none]
\end{frame}
$else$
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
$endif$
$endif$
$for(include-after)$
$include-after$
$endfor$
\end{document}

@ -1,326 +0,0 @@
License
=======
Author: Cedric Mesnil <cslashm@gmail.com>
License:
| Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
| Licensed under the Apache License, Version 2.0 (the "License");
| you may not use this file except in compliance with the License.
| You may obtain a copy of the License at
|
| http://www.apache.org/licenses/LICENSE-2.0
|
| Unless required by applicable law or agreed to in writing, software
| distributed under the License is distributed on an "AS IS" BASIS,
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
| See the License for the specific language governing permissions and
| limitations under the License.
Introduction
============
OpenPGP Card Application v3.0 add-ons summary
---------------------------------------------
Key management:
~~~~~~~~~~~~~~~
OpenPGP Application manage four keys for cryptographic operation (PSO) plus two
for secure channel.
The first four keys are defined as follow:
- One asymmetric signature private key (RSA or EC), named 'sig';
- One asymmetric decryption private key (RSA or EC), named 'dec'
- One asymmetric authentication private key (RSA or EC), named 'aut'
- One symmetric decryption private key (AES), named 'sym0'
The 3 first asymmetric keys can be either randomly generated on-card or
explicitly put from outside.
The fourth is put from outside.
It's never possible to retrieve private key from the card.
This add-on specification propose a solution to derive those keys from the
master seed managed by the Ledger Token.
This allow owner to restore a broken token without the needs to keep track of keys
outside the card.
Moreover this add-on specification propose to manage multiple set of the
four previously described keys.
Random number generation
~~~~~~~~~~~~~~~~~~~~~~~~
OpenPGP Application provides, as optional feature, to generate random bytes.
This add-on specification propose new type of random generation:
- random prime number generation
- seeded random number
- seeded prime number generation
Key Backup
~~~~~~~~~~
A full keybackup mecanism is provided.
GPG-ledger
==========
Definitions
-----------
- The application is named GPG-ledger
- A keys set is named 'keys slot'
How
---
Deterministic key derivation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The deterministic key derivation process relies on the BIP32 scheme.
The master install path of GPG-ledger is set to /0x80'GPG', aka /80475047
Deterministic key derivation maybe activated in:
Settings->Seed Mode->Set on
This activation remains effective until *set off* is selected or the application
ends.
The key management remains the same if seed mode is on or off, i.e. key are stored in memory key containers. So their is no perfomance inpact when using seeded keys.
Seeded keys are generated as follow:
**Step1**:
For a given keys slot n, starting from 1, a seed is first derived with the following path
Sn = BIP32_derive (/0x80475047/n)
**Step2**:
Then specific seeds are derived with the SHA3-XOF function for each of the four key :
Sk[i] = SHA3-XOF(SHA256(Sn \| <key_name> \| int16(i)), length)
Sn is the dedicated slot seed from step 1.
key_name is one of 'sig ','dec ', 'aut ', 'sym0', each four characters.
i is the index, starting from 1, of the desired seed (see below)
**Step 3**:
*RSA key are generated as follow* :
Generate two seed Sp, Sq in step2 with :
- i € {1,2}
- length equals to half key size
Generate two prime numbers p, q :
- p = next_prime(Sp)
- q = next_prime(Sq)
Generate RSA key pair as usual.
- choose e
- n = p*q
- d = inv(e) mod (p-1)(q-1)
*ECC key genration* :
Generate one seed Sd in step2 with :
- i = 1
- length equals to curve size
Generate ECC key pair :
- d = Sd
- W = d.G
*AES key generation* :
Generate one seed Sd in step2 with :
- i = 1
- length equals to 16
Generate AES key :
- k = Sk
Deterministic random number
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The deterministic random number generation relies on the BIP32 scheme.
The master install path of GPG-ledger is set to /0x80'GPG', aka /80475047
**Random prime number generation** :
For a given length *L*:
- generate random number r of *L* bytes.
- generate rp = next_prime(r)
- return rp
**Seeded random number** :
For a given length *L* and seed *S*:
- generate Sr = BIP32_derive(/0x80475047/0x0F0F0F0F)
- generate r = SHA3-XOF(SHA256(Sr \| 'rnd' \| S), L)
- return r
**Seeded prime number generation** :
For a given length *L* and seed *S*:
- generate r as for "Seeded random number"
- generate rp = next_prime(r)
- return rp
Key Backup & Restore
~~~~~~~~~~~~~~~~~~~~
In order to backup/restore private key the commands `put_data` and
`get_data` accept the tag `B6` (signature key), `B8`(encryption key),
`A4` (authentication).
put_data command accept the exact output of get_data. The get_data command
return both the public and private key.
For security and confidentiality private key is returned encryped in AES.
The key used is derived according to previously described AES key derivation
with name 'key '.
The data payload is formatted as follow:
+-------+--------------------------------------------------+
| size | Description |
+=======+==================================================+
| 4 | OS Target ID |
+-------+--------------------------------------------------+
| 4 | API Level |
+-------+--------------------------------------------------+
| 4 | compliance Level |
+-------+--------------------------------------------------+
| 4 | public key size |
+-------+--------------------------------------------------+
| var | public key |
+-------+--------------------------------------------------+
| 4 | private key size |
+-------+--------------------------------------------------+
| var | encrypted private key |
+-------+--------------------------------------------------+
APDU Modification
-----------------
Key Slot management
~~~~~~~~~~~~~~~~~~~~
Key slots are managed by data object 01F1 and 01F2 witch are
manageable by PUT/GET DATA command as for others DO and organized as follow.
On application reset, the *01F2* content is set to *Default Slot* value
of *01F1*.
*01F1:*
+------+--------------------------------------------------+--------+
|bytes | description | R/W |
+======+==================================================+========+
| 1 | Number of slot | R |
+------+--------------------------------------------------+--------+
| 2 | Default slot | R/W |
+------+--------------------------------------------------+--------+
| 3 | Allowed slot selection method | R/W |
+------+--------------------------------------------------+--------+
Byte 3 is endoced as follow:
+----+----+----+----+----+----+----+----+-------------------------+
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | \- | x | selection by APDU |
+----+----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | x | \- | selection by screen |
+----+----+----+----+----+----+----+----+-------------------------+
*01F2:*
+------+--------------------------------------------------+--------+
|bytes | Description | R/W |
+======+==================================================+========+
| 1 | Current slot | R/W |
+------+--------------------------------------------------+--------+
*01F0:*
+------+--------------------------------------------------+--------+
|bytes | Description | R/W |
+======+==================================================+========+
| 1-3 | 01F1 content | R |
+------+--------------------------------------------------+--------+
| 4 | 01F2 content | R |
+------+--------------------------------------------------+--------+
*Access Conditions:*
+-------+------------+-------------+
| DO | Read | Write |
+=======+============+=============+
| 01F0 | Always | Never |
+-------+------------+-------------+
| 01F1 | Always | Verify PW3 |
+-------+------------+-------------+
| 01F2 | Always | Verify PW2 |
+-------+------------+-------------+
Deterministic key derivation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
P2 parameter of GENERATE ASYMMETRIC KEY PAIR is set to (hex value):
- 00 for true random key generation
- 01 for seeded random key
Deterministic random number
~~~~~~~~~~~~~~~~~~~~~~~~~~~
P1 parameter of GET CHALLENGE is a bits field encoded as follow:
+----+-----+----+----+----+----+----+----+-------------------------+
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
+----+-----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | \- | x | prime random |
+----+-----+----+----+----+----+----+----+-------------------------+
| \- | \- | \- | \- | \- | \- | x | \- | seeded random |
+----+-----+----+----+----+----+----+----+-------------------------+
When bit b2 is set, data field contains the seed and P2 contains
the length of random bytes to generate.
Other minor add-on
------------------
GnuPG use both fingerprints and serial number to identfy key on card.
So, the put data command is able to modify the AID file with '4F' tag.
In that case the data field shall be four bytes length and shall contain
the new serial number. '4F' is protected by PW3 (admin) PIN.

@ -1,196 +0,0 @@
Step1: ...
-----
Jump into any temp dir
Step2: install nanos
-----
Do a fresh install of gpg application 1.1.0 from google app manager
Step3: setup conf
-----
Create a 'manual-test' directory
$ mkdir manual-test
Create a 'manual-test/gnupg'
$ mkdir manual-test/gnupg
Create a 'manual-test/gnupg/scdaemon.conf' file with content:
reader-port "Ledger Token [Nano S] (0001) 01 00"
allow-admin
card-timeout 1
debug-level expert
debug 11
log-file /tmp/scdaemon.log
Jump into manual-test dir
Step4: change to host pin style
-----
Launch gpg NanoS application and:
$ killall scdaemon gpg-agent
$ gpg2 --homedir `pwd`/gnupg --card-edit
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
gpg: keybox '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/pubring.kbx' created
Reader ...........: Ledger Token [Nano S] (0001) 01 00
Application ID ...: D2760001240103002C97DDD38BA90000
Version ..........: 3.0
Manufacturer .....: unknown
Serial number ....: DDD38BA9
Name of cardholder: [not set]
Language prefs ...: [not set]
Sex ..............: unspecified
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
gpg/card> verify
Reader ...........: Ledger Token [Nano S] (0001) 01 00
Application ID ...: D2760001240103002C97DDD38BA90000
Version ..........: 3.0
Manufacturer .....: unknown
Serial number ....: DDD38BA9
Name of cardholder: [not set]
Language prefs ...: [not set]
Sex ..............: unspecified
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
gpg/card>
Then on nanos, goto settings->PIN mode, and select 'Host'
Then on nanos, goto settings->PIN mode, and select 'Set as default'
unplug and replug the nanos
relaunch the openpgp application
Goto settings->PIN mode, and check you have "Host # +" (DASH and PLUS)
Step5: create 2048bits RSA keys
-----
In 'manual-test' directory, ask key generation. Nota that during this phase PIN has to be validate on Nanos
$ killall scdaemon gpg-agent
$ gpg2 --homedir `pwd`/gnupg --card-edit
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
Reader ...........: Ledger Token [Nano S] (0001) 01 00
Application ID ...: D2760001240103002C97DDD38BA90000
Version ..........: 3.0
Manufacturer .....: unknown
Serial number ....: DDD38BA9
Name of cardholder: [not set]
Language prefs ...: [not set]
Sex ..............: unspecified
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 0 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]
gpg/card> admin
Admin commands are allowed
gpg/card> generate
Make off-card backup of encryption key? (Y/n) n
Please note that the factory settings of the PINs are
PIN = '123456' Admin PIN = '12345678'
You should change them using the command --change-pin
What keysize do you want for the Signature key? (2048) 2048
What keysize do you want for the Encryption key? (2048) 2048
What keysize do you want for the Authentication key? (2048) 2048
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: testkey
Email address:
Comment:
You selected this USER-ID:
"testkey"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
gpg: /home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/trustdb.gpg: trustdb created
gpg: key 5ED17DF289C757A2 marked as ultimately trusted
gpg: directory '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/openpgp-revocs.d/7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2.rev'
public and secret key created and signed.
gpg/card> quit
pub rsa2048 2017-10-03 [SC]
7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2
uid testkey
sub rsa2048 2017-10-03 [A]
sub rsa2047 2017-10-03 [E]
Step6: encrypt/decrypt
-----
encrypt
$ killall scdaemon gpg-agent
$ echo CLEAR > foo.txt
$ gpg2 --homedir `pwd`/gnupg -e -r testkey foo.txt
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
Force pin to asked
$ killall gpg-agent scdaemon
decrypt
$ gpg2 --homedir `pwd`/gnupg foo.txt.gpg
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
gpg: encrypted with 2047-bit RSA key, ID 602FE5EB7BFA4B00, created 2017-10-03
"testkey"
File 'foo.txt' exists. Overwrite? (y/N) y
Step7: pin on screen
------
Restart from Step1, but skip step4

@ -0,0 +1,24 @@
#!/bin/bash
NAMES=()
NAMES+=(user/app-openpgp)
NAMES+=(developer/gpgcard-addon)
OPTIONS=()
OPTIONS+=("--standalone")
OPTIONS+=("--from=rst")
OPTIONS+=("--to=latex")
OPTIONS+=("--variable=papersize:A4")
OPTIONS+=("--variable=geometry:margin=1in")
OPTIONS+=("--variable=fontsize:10pt")
OPTIONS+=("--toc")
OPTIONS+=("--number-sections")
OPTIONS+=("--template=template.latex")
for name in "${NAMES[@]}"; do
rm -f "${name}.pdf"
dir=$(dirname "${name}")
file=$(basename "${name}")
(cd "${dir}"; pandoc ${OPTIONS[@]} --output="${file}.pdf" "${file}.rst")
done

@ -0,0 +1,33 @@
--- libccid_Info.plist_org 2023-10-17 10:50:56.030148081 +0200
+++ libccid_Info.plist_test 2024-01-16 15:11:54.798891142 +0100
@@ -462,6 +462,8 @@
<string>0x2D25</string>
<string>0x2C97</string>
<string>0x2C97</string>
+ <string>0x2C97</string>
+ <string>0x2C97</string>
<string>0x17EF</string>
<string>0x17EF</string>
<string>0x17EF</string>
@@ -1005,8 +1007,10 @@
<string>0x43A9</string>
<string>0x0000</string>
<string>0x0001</string>
- <string>0x0001</string>
- <string>0x0004</string>
+ <string>0x1009</string>
+ <string>0x4009</string>
+ <string>0x5009</string>
+ <string>0x6009</string>
<string>0x6007</string>
<string>0x6055</string>
<string>0x6111</string>
@@ -1552,6 +1556,8 @@
<string>KRONEGGER Micro Core Platform</string>
<string>Ledger Nano S</string>
<string>Ledger Nano X</string>
+ <string>Ledger Nano S Plus</string>
+ <string>Ledger Stax</string>
<string>Lenovo Lenovo USB Smartcard Keyboard</string>
<string>Lenovo Lenovo USB Smartcard Keyboard</string>
<string>Lenovo Lenovo Smartcard Wired Keyboard II</string>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,275 +0,0 @@
\documentclass[$if(fontsize)$$fontsize$,$endif$$if(lang)$$babel-lang$,$endif$$if(papersize)$$papersize$paper,$endif$$for(classoption)$$classoption$$sep$,$endfor$,towside]{report}
$if(fontfamily)$
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
$else$
\usepackage{lmodern}
$endif$
$if(linestretch)$
\usepackage{setspace}
\setstretch{$linestretch$}
$endif$
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\usepackage{fixltx2e} % provides \textsubscript
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
\usepackage[utf8]{inputenc}
$if(euro)$
\usepackage{eurosym}
$endif$
\else % if luatex or xelatex
\ifxetex
\usepackage{mathspec}
\else
\usepackage{fontspec}
\fi
\defaultfontfeatures{Ligatures=TeX,Scale=MatchLowercase}
$for(fontfamilies)$
\newfontfamily{$fontfamilies.name$}[$fontfamilies.options$]{$fontfamilies.font$}
$endfor$
$if(euro)$
\newcommand{\euro}{€}
$endif$
$if(mainfont)$
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
$endif$
$if(sansfont)$
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
$endif$
$if(monofont)$
\setmonofont[Mapping=tex-ansi$if(monofontoptions)$,$for(monofontoptions)$$monofontoptions$$sep$,$endfor$$endif$]{$monofont$}
$endif$
$if(mathfont)$
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
$endif$
$if(CJKmainfont)$
\usepackage{xeCJK}
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
$endif$
\fi
% use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
% use microtype if available
\IfFileExists{microtype.sty}{%
\usepackage{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
$if(geometry)$
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
$endif$
\usepackage[unicode=true]{hyperref}
$if(colorlinks)$
\PassOptionsToPackage{usenames,dvipsnames}{color} % color is loaded by hyperref
$endif$
\hypersetup{
$if(title-meta)$
pdftitle={$title-meta$},
$endif$
$if(author-meta)$
pdfauthor={$author-meta$},
$endif$
$if(keywords)$
pdfkeywords={$for(keywords)$$keywords$$sep$; $endfor$},
$endif$
$if(colorlinks)$
colorlinks=true,
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
$else$
pdfborder={0 0 0},
$endif$
breaklinks=true}
\urlstyle{same} % don't use monospace font for urls
$if(lang)$
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
$if(babel-newcommands)$
$babel-newcommands$
$endif$
\else
\usepackage{polyglossia}
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
$for(polyglossia-otherlangs)$
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
$endfor$
\fi
$endif$
$if(natbib)$
\usepackage{natbib}
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
$endif$
$if(biblatex)$
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
$for(bibliography)$
\addbibresource{$bibliography$}
$endfor$
$endif$
$if(listings)$
\usepackage{listings}
$endif$
$if(lhs)$
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
$endif$
$if(highlighting-macros)$
$highlighting-macros$
$endif$
$if(verbatim-in-note)$
\usepackage{fancyvrb}
\VerbatimFootnotes % allows verbatim text in footnotes
$endif$
$if(tables)$
\usepackage{longtable,booktabs}
$endif$
$if(graphics)$
\usepackage{graphicx,grffile,float}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
$endif$
$if(links-as-notes)$
% Make links footnotes instead of hotlinks:
\renewcommand{\href}[2]{#2\footnote{\url{#1}}}
$endif$
$if(strikeout)$
\usepackage[normalem]{ulem}
% avoid problems with \sout in headers with hyperref:
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
$endif$
$if(indent)$
$else$
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}
}
$endif$
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
$if(numbersections)$
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
$else$
\setcounter{secnumdepth}{0}
$endif$
$if(subparagraph)$
$else$
% Redefines (sub)paragraphs to behave more like sections
\ifx\paragraph\undefined\else
\let\oldparagraph\paragraph
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
\fi
\ifx\subparagraph\undefined\else
\let\oldsubparagraph\subparagraph
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
\fi
$endif$
$if(dir)$
\ifxetex
% load bidi as late as possible as it modifies e.g. graphicx
$if(latex-dir-rtl)$
\usepackage[RTLdocument]{bidi}
$else$
\usepackage{bidi}
$endif$
\fi
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\TeXXeTstate=1
\newcommand{\RL}[1]{\beginR #1\endR}
\newcommand{\LR}[1]{\beginL #1\endL}
\newenvironment{RTL}{\beginR}{\endR}
\newenvironment{LTR}{\beginL}{\endL}
\fi
$endif$
$for(header-includes)$
$header-includes$
$endfor$
\title{OpenPGP Card Application}
\author{Cedric Mesnil cedric@ledger.fr}
\date{$date$}
\begin{document}
\begin{titlepage}
\centering
% \includegraphics[width=0.15\textwidth]{example-image-1x1}\par\vspace{1cm}
{\scshape\LARGE OpenPGP Card Application \par}
{\scshape \LARGE User Guide \par}
\vspace{1cm}
% {\scshape\Large Ledger SAS \par}
\vspace{1cm}
\begin{figure}[h]
\includegraphics{../common/LogoLedgerV.png}
\centering
\end{figure}
{\Large\itshape Cédric Mesnil (cedric@ledger.fr)\par}
\vfill
% Bottom of the page
{\large \today\par}
\end{titlepage}
$if(abstract)$
\begin{abstract}
$abstract$
\end{abstract}
$endif$
$for(include-before)$
$include-before$
$endfor$
$if(toc)$
{
$if(colorlinks)$
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$black$endif$}
$endif$
\setcounter{tocdepth}{$toc-depth$}
\tableofcontents
}
$endif$
$if(lot)$
\listoftables
$endif$
$if(lof)$
\listoffigures
$endif$
$body$
$if(natbib)$
$if(bibliography)$
$if(biblio-title)$
$if(book-class)$
\renewcommand\bibname{$biblio-title$}
$else$
\renewcommand\refname{$biblio-title$}
$endif$
$endif$
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
$endif$
$endif$
$if(biblatex)$
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
$endif$
$for(include-after)$
$include-after$
$endfor$
\end{document}

@ -1,9 +0,0 @@
#!/bin/bash
NAME=blue-app-openpgp-card
rm -f ${NAME}.pdf
OUTPUT_FORMAT=rst+smart
pandoc -s --template=${NAME}.template -f ${OUTPUT_FORMAT} -V geometry:a4paper -V geometry:margin=1in -V fontsize=10pt -t latex --toc -N -o ${NAME}.pdf ${NAME}.rst

@ -0,0 +1,520 @@
% Options for packages loaded elsewhere
\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref}
\PassOptionsToPackage{hyphens}{url}
$if(colorlinks)$
\PassOptionsToPackage{dvipsnames,svgnames*,x11names*}{xcolor}
$endif$
$if(dir)$
$if(latex-dir-rtl)$
\PassOptionsToPackage{RTLdocument}{bidi}
$endif$
$endif$
$if(CJKmainfont)$
\PassOptionsToPackage{space}{xeCJK}
$endif$
%
\documentclass[
$if(fontsize)$
$fontsize$,
$endif$
$if(lang)$
$babel-lang$,
$endif$
$if(papersize)$
$papersize$paper,
$endif$
$if(beamer)$
ignorenonframetext,
$if(handout)$
handout,
$endif$
$if(aspectratio)$
aspectratio=$aspectratio$,
$endif$
$endif$
$for(classoption)$
$classoption$$sep$,
$endfor$
]{$documentclass$}
$if(beamer)$
$if(background-image)$
\usebackgroundtemplate{%
\includegraphics[width=\paperwidth]{$background-image$}%
}
$endif$
\usepackage{pgfpages}
\setbeamertemplate{caption}[numbered]
\setbeamertemplate{caption label separator}{: }
\setbeamercolor{caption name}{fg=normal text.fg}
\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$
$for(beameroption)$
\setbeameroption{$beameroption$}
$endfor$
% Prevent slide breaks in the middle of a paragraph
\widowpenalties 1 10000
\raggedbottom
$if(section-titles)$
\setbeamertemplate{part page}{
\centering
\begin{beamercolorbox}[sep=16pt,center]{part title}
\usebeamerfont{part title}\insertpart\par
\end{beamercolorbox}
}
\setbeamertemplate{section page}{
\centering
\begin{beamercolorbox}[sep=12pt,center]{part title}
\usebeamerfont{section title}\insertsection\par
\end{beamercolorbox}
}
\setbeamertemplate{subsection page}{
\centering
\begin{beamercolorbox}[sep=8pt,center]{part title}
\usebeamerfont{subsection title}\insertsubsection\par
\end{beamercolorbox}
}
\AtBeginPart{
\frame{\partpage}
}
\AtBeginSection{
\ifbibliography
\else
\frame{\sectionpage}
\fi
}
\AtBeginSubsection{
\frame{\subsectionpage}
}
$endif$
$endif$
$if(beamerarticle)$
\usepackage{beamerarticle} % needs to be loaded first
$endif$
$if(fontfamily)$
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
$else$
\usepackage{lmodern}
$endif$
$if(linestretch)$
\usepackage{setspace}
$endif$
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\usepackage{pifont}
\usepackage{newunicodechar}
\newunicodechar{✓}{\ding{51}}
\newunicodechar{✗}{\ding{55}}
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{textcomp} % provide euro and other symbols
\else % if luatex or xetex
$if(mathspec)$
\ifxetex
\usepackage{mathspec}
\else
\usepackage{unicode-math}
\fi
$else$
\usepackage{unicode-math}
$endif$
\defaultfontfeatures{Scale=MatchLowercase}
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
$if(mainfont)$
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
$endif$
$if(sansfont)$
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
$endif$
$if(monofont)$
\setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$}
$endif$
$for(fontfamilies)$
\newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$}
$endfor$
$if(mathfont)$
$if(mathspec)$
\ifxetex
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
\else
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
\fi
$else$
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
$endif$
$endif$
$if(CJKmainfont)$
\ifxetex
\usepackage{xeCJK}
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
\fi
$endif$
$if(luatexjapresetoptions)$
\ifluatex
\usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset}
\fi
$endif$
$if(CJKmainfont)$
\ifluatex
\usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec}
\setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
\fi
$endif$
\fi
$if(beamer)$
$if(theme)$
\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$}
$endif$
$if(colortheme)$
\usecolortheme{$colortheme$}
$endif$
$if(fonttheme)$
\usefonttheme{$fonttheme$}
$endif$
$if(mainfont)$
\usefonttheme{serif} % use mainfont rather than sansfont for slide text
$endif$
$if(innertheme)$
\useinnertheme{$innertheme$}
$endif$
$if(outertheme)$
\useoutertheme{$outertheme$}
$endif$
$endif$
% Use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
\IfFileExists{microtype.sty}{% use microtype if available
\usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
$if(indent)$
$else$
\makeatletter
\@ifundefined{KOMAClassName}{% if non-KOMA class
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
}{% if KOMA class
\KOMAoptions{parskip=half}}
\makeatother
$endif$
$if(verbatim-in-note)$
\usepackage{fancyvrb}
$endif$
\usepackage{xcolor}
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
\hypersetup{
$if(title-meta)$
pdftitle={$title-meta$},
$endif$
$if(author-meta)$
pdfauthor={$author-meta$},
$endif$
$if(lang)$
pdflang={$lang$},
$endif$
$if(subject)$
pdfsubject={$subject$},
$endif$
$if(keywords)$
pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$},
$endif$
$if(colorlinks)$
colorlinks=true,
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
filecolor=$if(filecolor)$$filecolor$$else$Maroon$endif$,
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
$else$
hidelinks,
$endif$
pdfcreator={LaTeX via pandoc}}
\urlstyle{same} % disable monospaced font for URLs
$if(verbatim-in-note)$
\VerbatimFootnotes % allow verbatim text in footnotes
$endif$
$if(geometry)$
$if(beamer)$
\geometry{$for(geometry)$$geometry$$sep$,$endfor$}
$else$
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
$endif$
$endif$
$if(beamer)$
\newif\ifbibliography
$endif$
$if(listings)$
\usepackage{listings}
\newcommand{\passthrough}[1]{#1}
\lstset{defaultdialect=[5.3]Lua}
\lstset{defaultdialect=[x86masm]Assembler}
$endif$
$if(lhs)$
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
$endif$
$if(highlighting-macros)$
$highlighting-macros$
$endif$
$if(tables)$
\usepackage{longtable,booktabs}
$if(beamer)$
\usepackage{caption}
% Make caption package work with longtable
\makeatletter
\def\fnum@table{\tablename~\thetable}
\makeatother
$else$
% Correct order of tables after \paragraph or \subparagraph
\usepackage{etoolbox}
\makeatletter
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
\makeatother
% Allow footnotes in longtable head/foot
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
\makesavenoteenv{longtable}
$endif$
$endif$
$if(graphics)$
\usepackage{graphicx}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
% Set default figure placement to htbp
\makeatletter
\def\fps@figure{htbp}
\makeatother
$endif$
$if(links-as-notes)$
% Make links footnotes instead of hotlinks:
\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}}
$endif$
$if(strikeout)$
\usepackage[normalem]{ulem}
% Avoid problems with \sout in headers with hyperref
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
$endif$
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
$if(numbersections)$
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
$else$
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
$endif$
$if(beamer)$
$else$
$if(block-headings)$
% Make \paragraph and \subparagraph free-standing
\ifx\paragraph\undefined\else
\let\oldparagraph\paragraph
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
\fi
\ifx\subparagraph\undefined\else
\let\oldsubparagraph\subparagraph
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
\fi
$endif$
$endif$
$if(pagestyle)$
\pagestyle{$pagestyle$}
$endif$
$for(header-includes)$
$header-includes$
$endfor$
$if(lang)$
\ifxetex
% Load polyglossia as late as possible: uses bidi with RTL languages (e.g. Hebrew, Arabic)
\usepackage{polyglossia}
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
$for(polyglossia-otherlangs)$
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
$endfor$
\else
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
$if(babel-newcommands)$
$babel-newcommands$
$endif$
\fi
$endif$
$if(dir)$
\ifxetex
% Load bidi as late as possible as it modifies e.g. graphicx
\usepackage{bidi}
\fi
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\TeXXeTstate=1
\newcommand{\RL}[1]{\beginR #1\endR}
\newcommand{\LR}[1]{\beginL #1\endL}
\newenvironment{RTL}{\beginR}{\endR}
\newenvironment{LTR}{\beginL}{\endL}
\fi
$endif$
$if(natbib)$
\usepackage[$natbiboptions$]{natbib}
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
$endif$
$if(biblatex)$
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
$for(bibliography)$
\addbibresource{$bibliography$}
$endfor$
$endif$
$if(csl-refs)$
\newlength{\cslhangindent}
\setlength{\cslhangindent}{1.5em}
\newenvironment{cslreferences}%
{$if(csl-hanging-indent)$\setlength{\parindent}{0pt}%
\everypar{\setlength{\hangindent}{\cslhangindent}}\ignorespaces$endif$}%
{\par}
$endif$
$if(title)$
\title{$title$$if(thanks)$\thanks{$thanks$}$endif$}
$endif$
$if(subtitle)$
$if(beamer)$
$else$
\usepackage{etoolbox}
\makeatletter
\providecommand{\subtitle}[1]{% add subtitle to \maketitle
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
}
\makeatother
$endif$
\subtitle{$subtitle$}
$endif$
\author{$for(author)$$author$$sep$ \and $endfor$}
\date{$date$}
$if(beamer)$
$if(institute)$
\institute{$for(institute)$$institute$$sep$ \and $endfor$}
$endif$
$if(titlegraphic)$
\titlegraphic{\includegraphics{$titlegraphic$}}
$endif$
$if(logo)$
\logo{\includegraphics{$logo$}}
$endif$
$endif$
\begin{document}
\begin{titlepage}
\centering
{\scshape\Huge OpenPGP Card Application \par}
{\scshape \huge User Guide \par}
\vspace{1cm}
{\scshape\LARGE Ledger SAS \par}
\vspace{2cm}
\includegraphics{../LogoLedger.png}
\vspace{1cm}
{\Large\itshape\url {https://github.com/LedgerHQ/app-openpgp}\par}
\vfill
{\large \today\par}
\end{titlepage}
$if(has-frontmatter)$
\frontmatter
$endif$
$if(title)$
$if(beamer)$
\frame{\titlepage}
$else$
\maketitle
$endif$
$if(abstract)$
\begin{abstract}
$abstract$
\end{abstract}
$endif$
$endif$
$for(include-before)$
$include-before$
$endfor$
$if(toc)$
$if(toc-title)$
\renewcommand*\contentsname{$toc-title$}
$endif$
$if(beamer)$
\begin{frame}[allowframebreaks]
$if(toc-title)$
\frametitle{$toc-title$}
$endif$
\tableofcontents[hideallsubsections]
\end{frame}
$else$
{
$if(colorlinks)$
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$}
$endif$
\setcounter{tocdepth}{$toc-depth$}
\tableofcontents
}
$endif$
$endif$
$if(lot)$
\listoftables
$endif$
$if(lof)$
\listoffigures
$endif$
$if(linestretch)$
\setstretch{$linestretch$}
$endif$
$if(has-frontmatter)$
\mainmatter
$endif$
$body$
$if(has-frontmatter)$
\backmatter
$endif$
$if(natbib)$
$if(bibliography)$
$if(biblio-title)$
$if(has-chapters)$
\renewcommand\bibname{$biblio-title$}
$else$
\renewcommand\refname{$biblio-title$}
$endif$
$endif$
$if(beamer)$
\begin{frame}[allowframebreaks]{$biblio-title$}
\bibliographytrue
$endif$
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
$if(beamer)$
\end{frame}
$endif$
$endif$
$endif$
$if(biblatex)$
$if(beamer)$
\begin{frame}[allowframebreaks]{$biblio-title$}
\bibliographytrue
\printbibliography[heading=none]
\end{frame}
$else$
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
$endif$
$endif$
$for(include-after)$
$include-after$
$endfor$
\end{document}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

@ -0,0 +1 @@
../icons/gpg_16px.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

@ -1,11 +0,0 @@
LICENSE
Files "manager_gnupg.xcf" and "manager_gnupg.png" are covered by Creative Commons Attribution-ShareAlike 3.0 Unported License.
See https://creativecommons.org/licenses/by-sa/3.0/legalcode .
Thanks to gnupg.org for the original images.
Others under this directory are covered by Apache License Version 2.0,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

@ -0,0 +1,8 @@
[app]
build_directory = "./"
sdk = "C"
devices = ["nanos", "nanox", "nanos+", "stax"]
[tests]
unit_directory = "./unit-tests/"
pytest_directory = "./tests/"

@ -0,0 +1,210 @@
#!/bin/bash
#
# script to check OpenPGP Application features
#
exeName=$(readlink "$0")
[[ -z ${exeName} ]] && exeName=$0
dirName=$(dirname "${exeName}")
gnupg_home_dir="$(realpath "${dirName}/gnupg")"
VERBOSE=false
EXPERT=false
#===============================================================================
#
# help - Prints script help and usage
#
#===============================================================================
# shellcheck disable=SC2154 # var is referenced but not assigned
help() {
echo
echo "Usage: ${exeName} <options>"
echo
echo "Options:"
echo
echo " -c <init|reset|card|encrypt|decryptsign|verify> : Requested command"
echo " -e : Expert mode mode"
echo " -v : Verbose mode"
echo " -h : Displays this help"
echo
exit 1
}
#===============================================================================
#
# reset - Kill running process, ensure clear next operation
#
#===============================================================================
reset() {
# Kill running process
killall scdaemon gpg-agent 2>/dev/null
}
#===============================================================================
#
# init - Init the gnupg config, start from an empty keyring
#
#===============================================================================
init() {
reset
# Cleanup old gnupg home directory
dir=$(basename "${gnupg_home_dir}")
rm -fr "${dir}" foo.txt*
mkdir "${dir}"
chmod 700 "${dir}"
{
echo reader-port \"Ledger token\"
echo allow-admin
echo enable-pinpad-varlen
echo card-timeout 1
} > "${dir}/scdaemon.conf"
if [[ ${EXPERT} == true ]]; then
{
echo log-file /tmp/scd.log
echo debug-level guru
echo debug-all
} >> "${dir}/scdaemon.conf"
fi
gpgconf --reload scdaemon
}
#===============================================================================
#
# card - Show/edit the card status and configuration
#
#===============================================================================
card() {
local expert_mode=""
[[ ${EXPERT} == true ]] && expert_mode="--expert"
gpg --homedir "${gnupg_home_dir}" ${expert_mode} --card-edit
}
#===============================================================================
#
# encrypt - Encrypt a clear file
#
#===============================================================================
encrypt() {
local recipient=""
local verbose_mode=""
reset
rm -fr foo*
echo CLEAR > foo.txt
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
recipient=$(gpg --homedir "${gnupg_home_dir}" --card-status | grep "General key info" | awk '{print $NF}')
echo "Encrypt with recipient '${recipient}'"
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --encrypt --recipient "${recipient}" foo.txt
}
#===============================================================================
#
# decrypt - Decrypt a file and compare with original clear content
#
#===============================================================================
decrypt() {
local verbose_mode=""
reset
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --decrypt foo.txt.gpg > foo_dec.txt
# Check with original clear file
diff foo.txt foo_dec.txt >/dev/null
if [[ $? -eq 0 ]]; then
echo "Success !"
else
echo "Decryption error!"
fi
rm -fr foo*
}
#===============================================================================
#
# sign - Sign a file
#
#===============================================================================
sign() {
local verbose_mode=""
reset
rm -fr foo*
echo CLEAR > foo.txt
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --sign foo.txt
}
#===============================================================================
#
# verify - Verify a file signature
#
#===============================================================================
verify() {
local verbose_mode=""
reset
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --verify foo.txt.gpg
rm -fr foo*
}
#===============================================================================
#
# Parsing parameters
#
#===============================================================================
if (($# < 1)); then
help
fi
while getopts ":c:evh" opt; do
case $opt in
c)
case ${OPTARG} in
init|reset|card|encrypt|decrypt|sign|verify)
CMD=${OPTARG}
;;
*)
echo "Wrong parameter '${OPTARG}'!"
exit 1
;;
esac
;;
e) EXPERT=true ;;
v) VERBOSE=true ;;
h) help ;;
\?) echo "Unknown option: -${OPTARG}" >&2; exit 1;;
: ) echo "Missing option argument for -${OPTARG}" >&2; exit 1;;
* ) echo "Unimplemented option: -${OPTARG}" >&2; exit 1;;
esac
done
#===============================================================================
#
# Main
#
#===============================================================================
# execute the command
${CMD}

@ -0,0 +1,117 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#*****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*****************************************************************************
import sys
from pathlib import Path
from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
from gpgapp.gpgcard import GPGCard, PassWord, GPGCardExcpetion
# ===============================================================================
# Parse command line options
# ===============================================================================
def get_argparser() -> Namespace:
"""Parse the commandline options"""
parser = ArgumentParser(
description="Backup/Restore OpenPGP App configuration",
epilog="Keys restore is only possible with SEED mode...",
formatter_class=RawTextHelpFormatter
)
parser.add_argument("--reader", type=str, default="Ledger",
help="PCSC reader name (default is '%(default)s') or 'speculos'")
parser.add_argument("--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)")
parser.add_argument("--pinpad", action="store_true",
help="PIN validation will be delegated to pinpad")
parser.add_argument("--adm-pin", metavar="PIN",
help="Admin PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
parser.add_argument("--user-pin", metavar="PIN",
help="User PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
parser.add_argument("--restore", action="store_true",
help="Perform a Restore instead of Backup")
parser.add_argument("--file", type=str, default="gpg_backup",
help="Backup/Restore file (default is '%(default)s')")
parser.add_argument("--seed-key", action="store_true",
help="After Restore, regenerate all keys, based on seed mode")
return parser.parse_args()
# ===============================================================================
# MAIN
# ===============================================================================
def entrypoint() -> None:
"""Main function"""
# Arguments parsing
# -----------------
args = get_argparser()
# Arguments checking
# ------------------
if not args.pinpad:
if not args.adm_pin or not args.user_pin:
print("If 'pinpad' is not use, 'userpin' and 'admpin' must be provided.")
sys.exit()
if args.restore is False:
if Path(args.file).is_file():
print(f"Provided backup file '{args.file}' already exist. Aborting!")
sys.exit()
# Processing
# ----------
try:
print(f"Connect to card '{args.reader}'...")
gpgcard: GPGCard = GPGCard()
gpgcard.connect(args.reader)
if not gpgcard.verify_pin(PassWord.PW1, args.user_pin, args.pinpad) or \
not gpgcard.verify_pin(PassWord.PW3, args.adm_pin, args.pinpad):
raise GPGCardExcpetion(0, "PIN not verified")
if args.slot:
gpgcard.select_slot(args.slot - 1)
gpgcard.get_all()
if args.restore:
gpgcard.restore(args.file)
print(f"Configuration restored from file '{args.file}'.")
if args.seed_key:
gpgcard.seed_key()
else:
gpgcard.backup(args.file)
print(f"Configuration saved in file '{args.file}'.")
gpgcard.disconnect()
except GPGCardExcpetion as err:
print(f"\n### Error {err.code}: {err.message}!\n")
if __name__ == "__main__":
entrypoint()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
#*****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*****************************************************************************
from enum import Enum, IntEnum
KEY_TEMPLATES = {
"rsa2048" : "010800002001",
"rsa3072" : "010C00002001",
# "rsa4096" : "011000002001", not supported yet
"nistp256": "132A8648CE3D030107",
"ed25519" : "162B06010401DA470F01",
"cv25519" : "122B060104019755010501"
}
KEY_OPERATIONS = {
"Export": 0x00, # Read and export a Public Key
"Generate": 0x80, # Generate a new Asymmetric key pair
"Read": 0x81, # Read Public Key
}
USER_SALUTATION = {
"Male": "1",
"Female": "2",
}
class KeyTypes(str, Enum):
"""Key types definition
OpenPGP Application manage four keys for cryptographic operation (PSO) plus two
for secure channel.
The first four keys are defined as follow:
- One asymmetric signature private key (RSA or EC), named 'sig'
- One asymmetric decryption private key (RSA or EC), named 'dec'
- One asymmetric authentication private key (RSA or EC), named 'aut'
- One symmetric decryption private key (AES), named 'sym0'
The 3 first asymmetric keys can be either randomly generated on-card or
explicitly put from outside.
The fourth is put from outside.
"""
# Asymmetric Signature Private Key (RSA or EC)
KEY_SIG = "SIG"
# Asymmetric Decryption Private Key (RSA or EC)
KEY_DEC = "DEC"
# Asymmetric Authentication Private Key (RSA or EC)
KEY_AUT = "AUT"
# Symmetric Decryption Key (AES)
class PubkeyAlgo(IntEnum):
""" Public-Key Algorithm IDs definition """
# https://www.rfc-editor.org/rfc/rfc4880#section-9.1
# RSA (Encrypt or Sign)
RSA = 1
# Elliptic Curve Diffie-Hellman
ECDH = 18
# Elliptic Curve Digital Signature Algorithm
ECDSA = 19
# Edwards-curve Digital Signature Algorithm
EDDSA = 22
class PassWord(IntEnum):
""" Password type definition """
# USER_PIN for only one PSO:CDS command
PW1 = 0x81
# USER_PIN for several attempts
PW2 = 0x82
# Admin PIN
PW3 = 0x83
class ErrorCodes:
""" Error codes definition """
err_list = {
0x6285: "Selected file in termination state",
0x6581: "Memory failure",
0x6600: "Security-related issues (reserved for UIF in this application)",
0x6700: "Wrong length (Lc and/or Le)",
0x6881: "Logical channel not supported",
0x6882: "Secure messaging not supported",
0x6883: "Last command of the chain expected",
0x6884: "Command chaining not supported",
0x6982: "Security status not satisfied",
0x6983: "Authentication method blocked",
0x6984: "Data Invalid",
0x6985: "Condition of use not satisfied",
0x6986: "Command not allowed",
0x6987: "Expected SM data objects missing",
0x6988: "SM data objects incorrect",
0x6A80: "Incorrect parameters in the data field",
0x6A82: "File or application not found",
0x6A86: "Incorrect P1-P2",
0x6A88: "Referenced data not found",
0x6B00: "Wrong parameters P1-P2",
0x6D00: "Instruction (INS) not supported",
0x6E00: "Class (CLA) not supported",
0x6F00: "Unknown Error",
0x9000: "Success",
}
ERR_SUCCESS = 0x9000
ERR_SW1_VALID = 0x61
ERR_INTERNAL = 0
class DataObject(IntEnum):
""" Data Objects definition """
# [Read/Write] Slot config
CMD_SLOT_CFG = 0x01F1
# [Read/Write] Slot selection
CMD_SLOT_CUR = 0x01F2
# [Read/Write] RSA Exponent
CMD_RSA_EXP = 0x01F8
# [Read] Full Application identifier (AID), ISO 7816-4
DO_AID = 0x4F
# [Read/Write] Login data
DO_LOGIN = 0x5E
# [Read/Write] Uniform resource locator (URL, as defined in RFC 1738)
DO_URL = 0x5F50
# [Read] Historical bytes, Card service data and Card capabilities
DO_HIST = 0x5F52
# [Read/Write] Optional DO for private use
DO_PRIVATE_01 = 0x0101
DO_PRIVATE_02 = 0x0102
DO_PRIVATE_03 = 0x0103
DO_PRIVATE_04 = 0x0104
# [Read] Cardholder Related Data
DO_CARDHOLDER_DATA = 0x65
# [Read/Write] Name according to ISO/IEC 7501-1)
DO_CARD_NAME = 0x5B
# [Read/Write] Language preferences (according to ISO 639)
DO_CARD_LANG = 0x5F2D
# [Read/Write] Salutation (according to ISO 5218)
DO_CARD_SALUTATION = 0x5F35
# [Read/Write] Digital signature
DO_SIG_KEY = 0xB6
# [Read/Write] Confidentiality
DO_DEC_KEY = 0xB8
# [Read/Write] Authentication
DO_AUT_KEY = 0xA4
# [Read] Application Related Data
DO_APP_DATA = 0x6E
# [Read] Extended length information (ISO 7816-4)
DO_EXT_LEN = 0x7F66
# [Read] Discretionary data objects
DO_DISCRET_DATA = 0x73
# [Read] Extended capabilities Flag list
DO_EXT_CAP = 0xC0
# [Read/Write] Algorithm attributes SIGnature
DO_SIG_ATTR = 0xC1
# [Read/Write] Algorithm attributes DECryption
DO_DEC_ATTR = 0xC2
# [Read/Write] Algorithm attributes AUThentication
DO_AUT_ATTR = 0xC3
# [Read/Write] PW status Bytes
DO_PW_STATUS = 0xC4
# [Read] Fingerprints (binary, 20 bytes (dec.) each for SIG, DEC, AUT)
DO_FINGERPRINTS = 0xC5
# [Read] List of CA-Fingerprints (binary, 20 bytes (dec.) each for SIG, DEC, AUT)
DO_CA_FINGERPRINTS = 0xC6
# [Write] Fingerprint for SIGnature key, format according to RFC 4880
DO_FINGERPRINT_WR_SIG = 0xC7
# [Write] Fingerprint for DECryption key, format according to RFC 4880
DO_FINGERPRINT_WR_DEC = 0xC8
# [Write] Fingerprint for AUThentication key, format according to RFC 4880
DO_FINGERPRINT_WR_AUT = 0xC9
# [Write] CA-Fingerprint for SIGnature key
DO_CA_FINGERPRINT_WR_SIG = 0xCA
# [Write] CA-Fingerprint for DECryption key
DO_CA_FINGERPRINT_WR_DEC = 0xCB
# [Write] CA-Fingerprint for AUThentication key
DO_CA_FINGERPRINT_WR_AUT = 0xCC
# [Read] List of generation dates. 4 bytes, Big Endian each for SIG, DEC, AUT
DO_KEY_DATES = 0xCD
# [Write] Generation date/time of SIGnature key (Big Endian, according to RFC 4880)
DO_DATES_WR_SIG = 0xCE
# [Write] Generation date/time of DECryption key (Big Endian, according to RFC 4880)
DO_DATES_WR_DEC = 0xCF
# [Write] Generation date/time of AUThentication key (Big Endian, according to RFC 4880)
DO_DATES_WR_AUT = 0xD0
# [Read] Security support template
DO_SEC_TEMPL = 0x7A
# [Read] Digital signature counter
DO_SIG_COUNT = 0x93
# [Write] Resetting Code
DO_RESET_CODE = 0xD3
# [Read/Write] User Interaction Flag (UIF) for PSO:CDS
DO_UIF_SIG = 0xD6
# [Read/Write] User Interaction Flag (UIF) for PSO:DEC
DO_UIF_DEC = 0xD7
# [Read/Write] User Interaction Flag (UIF) for PSO:AUT
DO_UIF_AUT = 0xD8
# [Read/Write] Cardholder certificate (each for AUT, DEC and SIG)
DO_CERT = 0x7F21
# [Read/Write] Asymmetric Key Pair
DO_PUB_KEY = 0x7F49
# [Read] General Feature management
DO_GEN_FEATURES = 0x7F74

@ -1,15 +0,0 @@
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

@ -1,25 +0,0 @@
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from gpgcard import GPGCard
gpgcard = GPGCard()
gpgcard.connect("pcsc:Ledger")
gpgcard.get_all()
gpgcard.verify_pin(0x81, "123456")
gpgcard.verify_pin(0x83, "12345678")
gpgcard.backup("backup_card.pickle")

@ -1,850 +0,0 @@
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
try:
from ledgerblue.comm import getDongle
from ledgerblue.commException import CommException
except :
pass
import binascii
from smartcard.System import readers
import sys
import datetime
import pickle
# decode level 0 of tlv|tlv|tlv
# return dico {t: v, t:v, ...}
#tlv: hexstring
def decode_tlv(tlv) :
tags = {}
while len(tlv) :
o = 0
l = 0
if (tlv[0] & 0x1F) == 0x1F:
t = (tlv[0]<<8)|tlv[1]
o = 2
else:
t = tlv[0]
o = 1
l = tlv[o]
if l & 0x80 :
if (l&0x7f) == 1:
l = tlv[o+1]
o += 2
if (l&0x7f) == 2:
l = (tlv[o+1]<<8)|tlv[o+2]
o += 3
else:
o += 1
v = tlv[o:o+l]
tags[t] = v
tlv = tlv[o+l:]
return tags
class GPGCardExcpetion(Exception):
pass
class GPGCard() :
def __init__(self):
self.reset()
self.log = False
def reset(self):
#token info
self.AID = b''
self.aid = b''
self.ext_length = b''
self.ext_capabilities = b''
self.histo_bytes = b''
self.PW_status = b''
#user info
self.login = b''
self.url = b''
self.name = b''
self.sex = b''
self.lang = b''
#keys info
self.cardholder_cert = b''
self.sig_attribute = b''
self.dec_attribute = b''
self.aut_attribute = b''
self.sig_fingerprints = b''
self.dec_fingerprints = b''
self.aut_fingerprints = b''
self.sig_CA_fingerprints = b''
self.dec_CA_fingerprints = b''
self.aut_CA_fingerprints = b''
self.sig_date = b''
self.dec_date = b''
self.aut_date = b''
self.cert_aut = b''
self.cert_dec = b''
self.cert_sig = b''
self.UIF_SIG = b''
self.UIF_DEC = b''
self.UIF_AUT = b''
self.digital_counter = b''
#private info
self.private_01 = b''
self.private_02 = b''
self.private_03 = b''
#keys
self.sig_key = b''
self.dec_key = b''
self.aut_key = b''
def connect(self, device):
self.token = None
if device.startswith("ledger:"):
self.token = getDongle(True)
self.exchange = self._exchange_ledger
self.disconnect = self._disconnect_ledger
elif device.startswith("pcsc:"):
allreaders = readers()
for r in allreaders:
rname = str(r)
#print('try: %s : %s'%(rname,device[5:]))
if rname.startswith(device[5:]):
r.createConnection()
self.token = r
self.connection = r.createConnection()
self.connection.connect()
self.exchange = self._exchange_pcsc
self.disconnect = self._disconnect_pcsc
else:
#print("No")
pass
if not self.token:
print("No token")
### APDU interface ###
def _exchange_ledger(self,cmd,data=None, sw=0x9000):
resp = b''
cond = True
while cond:
try:
resp = resp + self.token.exchange(cmd,300)
sw = 0x9000
cond = False
except CommException as e:
if (e.data) :
resp = resp + e.data
sw = e.sw
if (sw&0xFF00) == 0x6100 :
cmd = binascii.unhexlify("00C00000%.02x"%(sw&0xFF))
else:
cond = False
return resp,sw
def log_apdu(self,l):
self.log = l
def alog(self, m,dt,sw=0):
if self.log:
print("%s %.04x %s"%(m,sw,''.join(["%.02x"%b for b in dt])))
def _exchange_pcsc(self,apdu, data=None, sw_expected=0x9000, sw_mask=0xFFFF):
if data:
data = [x for x in data]
apdu = [x for x in apdu]
#send
if data:
while len(data) > 0xFE:
apdux = apdu[0:5]+[0xfe]+data[0:0xFE]
apdux[0] |= 0x10
self.alog('send', apdux)
resp, sw1, sw2 = self.connection.transmit(apdux)
sw = (sw1<<8)|sw2
self.alog('recv',resp,sw)
if sw != 0x9000:
return resp,sw
data = data[0xFE:]
apdu = apdu+[len(data)]+data
self.alog('send', apdu)
resp, sw1, sw2 = self.connection.transmit(apdu)
sw = (sw1<<8)|sw2
self.alog('recv', resp, sw)
#receive
while sw1==0x61:
apdu = binascii.unhexlify(b"00c00000%.02x"%sw2)
apdu = [x for x in apdu]
self.alog('send', apdu)
resp2, sw1, sw2 = self.connection.transmit(apdu)
sw = (sw1<<8)|sw2
self.alog('recv', resp2, sw)
resp = resp + resp2
resp = bytes(resp)
sw = (sw1<<8)|sw2
if sw&sw_mask == sw_expected:
return resp,sw
raise GPGCardExcpetion(binascii.hexlify(resp), "%.04x"%sw)
def _disconnect_ledger(self):
return self.token.close()
def _disconnect_pcsc(self):
r = self.connection.disconnect()
#self.connection.releaseContext()
return r
def select(self):
apdu = binascii.unhexlify(b"00A4040006D27600012401")
return self.exchange(apdu)
def activate(self):
apdu = binascii.unhexlify(b"00440000")
return self.exchange(apdu)
def terminate(self):
apdu = binascii.unhexlify(b"00E60000")
return self.exchange(apdu)
def get_log(self):
apdu = binascii.unhexlify(b"00040000")
return self.exchange(apdu)
def get_data(self,tag):
apdu = binascii.unhexlify(b"00CA%.04x00"%tag)
return self.exchange(apdu)
def put_data(self,tag,value):
return self.exchange(binascii.unhexlify(b"00DA%.04x"%tag), value)
def verify(self,id,value, pinpad=False):
if pinpad:
apdu = binascii.unhexlify(b"EF2000%.02x00"%id)
else:
apdu = binascii.unhexlify(b"002000%.02x%.02x"%(id,len(value)))+value
return self.exchange(apdu)
def change_reference_data(self,id,value,new_value):
lc = len(value)+len(new_value)
apdu = binascii.unhexlify(b"002400%.02x%.02x"%(id,lc))+value+new_value
return self.exchange(apdu)
def reset_retry_counter(self,RC,new_value):
if len(RC)==0:
p1 = 2
else:
p1 = 0
lc = len(RC)+len(new_value)
apdu = binascii.unhexlify(b"002C%02x81%.02x"%(p1,lc))+RC+new_value
return self.exchange(apdu)
def generate_asym_key_pair(self, mode, key):
apdu = binascii.unhexlify(b"0047%02x0002%.04x"%(mode,key))
return self.exchange(apdu)
### API interfaces ###
def get_all(self, with_key=False):
self.reset()
self.slot,sw = self.get_data(0x01F2)
self.AID,sw = self.get_data(0x4f)
self.login ,sw = self.get_data(0x5e)
self.url,sw = self.get_data(0x5f50)
self.histo_bytes,sw = self.get_data(0x5f52)
cardholder,sw = self.get_data(0x65)
tags = decode_tlv(cardholder)
if 0x5b in tags:
self.name = tags[0x5b]
if 0x5f35 in tags:
self.sex = tags[0x5f35]
if 0x5f35 in tags:
self.lang = tags[0x5f2d]
application_data,sw = self.get_data(0x6E)
tags = decode_tlv(application_data)
if 0x7f66 in tags:
self.ext_length = tags[0x7f66]
if 0x73 in tags:
dicretionary_data = tags[0x73]
tags = decode_tlv(dicretionary_data)
if 0xc0 in tags:
self.ext_capabilities = tags[0xC0]
if 0xc4 in tags:
self.PW_status = tags[0xC4]
if 0xC1 in tags:
self.sig_attribute = tags[0xC1]
if 0xC2 in tags:
self.dec_attribute = tags[0xC2]
if 0xC3 in tags:
self.aut_attribute = tags[0xC3]
if 0xC5 in tags:
fingerprints = tags[0xC5]
self.sig_fingerprints = fingerprints[0:20]
self.dec_fingerprints = fingerprints[20:40]
self.aut_fingerprints = fingerprints[40:60]
if 0xC6 in tags:
fingerprints = tags[0xC6]
self.sig_CA_fingerprints = fingerprints[0:20]
self.dec_CA_fingerprints = fingerprints[20:40]
self.aut_CA_fingerprints = fingerprints[40:60]
if 0xcd in tags:
dates = tags[0xCD]
self.sig_date = dates[0:4]
self.dec_date = dates[4:8]
self.aut_date = dates[8:12]
self.cardholder_cert = self.get_data(0x7f21)
self.UIF_SIG,sw = self.get_data(0xD6)
self.UIF_DEC,sw = self.get_data(0xD7)
self.UIF_AUT,sw = self.get_data(0xD8)
sec_template,sw = self.get_data(0x7A)
tags = decode_tlv(sec_template)
if 0x93 in tags:
self.digital_counter = tags[0x93]
self.private_01,sw = self.get_data(0x0101)
self.private_02,sw = self.get_data(0x0102)
self.private_03,sw = self.get_data(0x0103)
self.private_04,sw = self.get_data(0x0104)
if with_key:
self.sig_key,sw = self.get_data(0x00B6)
self.dec_key,sw = self.get_data(0x00B8)
self.aut_key,sw = self.get_data(0x00A4)
return True
def set_all(self):
self.put_data(0x4f, self.AID[10:14])
self.put_data(0x0101, self.private_01)
self.put_data(0x0102, self.private_02)
self.put_data(0x0103, self.private_03)
self.put_data(0x0104, self.private_04)
self.put_data(0x5b, self.name)
self.put_data(0x5e, self.login)
self.put_data(0x5f2d, self.lang)
self.put_data(0x5f35, self.sex)
self.put_data(0x5f50, self.url)
self.put_data(0xc1, self.sig_attribute)
self.put_data(0xc2, self.dec_attribute)
self.put_data(0xc3, self.aut_attribute)
self.put_data(0xc4, self.PW_status)
self.put_data(0xc7, self.sig_fingerprints)
self.put_data(0xc8, self.dec_fingerprints)
self.put_data(0xc9, self.aut_fingerprints)
self.put_data(0xca, self.sig_CA_fingerprints)
self.put_data(0xcb, self.dec_CA_fingerprints)
self.put_data(0xcc, self.aut_CA_fingerprints)
self.put_data(0xce, self.sig_date)
self.put_data(0xcf, self.dec_date)
self.put_data(0xd0, self.aut_date)
#self.put_data(0x7f21, self.cardholder_cert)
self.put_data(0xd6, self.UIF_SIG)
self.put_data(0xd7, self.UIF_DEC)
self.put_data(0xd8, self.UIF_AUT)
if len(self.sig_key):
self.put_data(0x00B6, self.sig_key)
if len(self.dec_key):
self.put_data(0x00B8, self.dec_key)
if len(self.aut_key):
self.put_data(0x00A4, self.aut_key)
return True
def _backup_file_name(self,file_name):
return file_name #file_name+"_slot%d"%(self.slot[0]+1)+".pickle"
def backup(self, file_name, with_key=False):
self.get_all(with_key)
file_name = self._backup_file_name(file_name)
f = open(file_name,mode='w+b')
pickle.dump(
(self.AID,
self.private_01, self.private_02, self.private_03, self.private_04,
self.name, self.login, self.sex, self.url,
self.sig_attribute, self.dec_attribute, self.aut_attribute,
self.PW_status,
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
self.sig_date, self.dec_date, self.aut_date,
self.cardholder_cert,
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
self.sig_key, self.dec_key, self.aut_key),
f, 2)
return True
def restore(self, file_name):
file_name = self._backup_file_name(file_name)
f = open(file_name,mode='r+b')
(self.AID,
self.private_01, self.private_02, self.private_03, self.private_04,
self.name, self.login, self.sex, self.url,
self.sig_attribute, self.dec_attribute, self.aut_attribute,
self.PW_status,
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
self.sig_date, self.dec_date, self.aut_date,
self.cardholder_cert,
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
self.sig_key, self.dec_key, self.aut_key) = pickle.load(f)
self.set_all()
return True
def seed_key(self):
apdu = binascii.unhexlify(b"0047800102B600")
self.exchange(apdu)
apdu = binascii.unhexlify(b"0047800102B800")
self.exchange(apdu)
apdu = binascii.unhexlify(b"0047800102A400")
self.exchange(apdu)
def decode_AID(self):
return {
'AID': ('AID' , "%x"%int.from_bytes(self.AID,'big')),
'RID': ('RID' , "%x"%int.from_bytes(self.AID[0:5],'big')),
'APP': ('application' , "%.02x"%self.AID[5]),
'VER': ('version' , "%.02x.%.02x"%(self.AID[6], self.AID[7])),
'MAN': ('manufacturer' , "%x"%int.from_bytes(self.AID[8:10],'big')),
'SER': ('serial' , "%x"%int.from_bytes(self.AID[10:14],'big'))
}
def decode_histo(self):
return {
'HIST': ('historical bytes', binascii.hexlify(self.histo_bytes))
}
def decode_extlength(self):
if self.ext_length:
return {
'CMD': ('Max command length' , "%d" %((self.ext_length[2]<<8)|self.ext_length[3])),
'RESP':( 'Max response length' , "%d" %((self.ext_length[6]<<8)|self.ext_length[7]))
}
else:
return {
'CMD': ('Max command length' , "unspecified"),
'RESP':( 'Max response length' ,"unspecified"),
}
def decode_capabilities(self):
d = {}
b1 = self.ext_capabilities[0]
if b1&0x80 :
if self.ext_capabilities[1] == 1:
d['SM'] = ('Secure Messaging', "yes: 128 bits")
elif self.ext_capabilities[1] == 2:
d['SM'] = ('Secure Messaging', "yes: 256 bits")
else:
d['SM'] = ('Secure Messaging', "yes: ?? bits")
else:
d['SM'] = ('Secure Messaging', "no")
if b1&0x40 :
d['CHAL'] = ('Get Challenge', "yes")
else:
d['CHAL'] = ('Get Challenge', "no")
if b1&0x20 :
d['KEY'] = ('Key import', "yes")
else:
d['KEY'] = ('Key import', "no")
if b1&0x10 :
d['PWS'] = ('PW status changeable', "yes")
else:
d['PWS'] = ('PW status changeable', "no")
if b1&0x08 :
d['PDO'] = ('Private DOs', "yes")
else:
d['PDO'] = ('Private DOs', "no")
if b1&0x04 :
d['ATTR'] = ('Algo attributes changeable', "yes")
else:
dd['ATTR'] = ('Algo attributes changeable', "no")
if b1&0x02 :
d['PSO'] = ('PSO:DEC support AES', "yes")
else:
d['PSO'] = ('PSO:DEC support AES', "no")
d['CHAL_MAX'] = ('Max GET_CHALLENGE length',
"%d"% ((self.ext_capabilities[2]<<8)|self.ext_capabilities[3]))
d['CERT_MAX'] = ('Max Cert length',
"%d"% ((self.ext_capabilities[4]<<8)|self.ext_capabilities[5]))
d['PDO_MAX'] = ('Max special DO length',
"%d"% ((self.ext_capabilities[6]<<8)|self.ext_capabilities[7]))
if self.ext_capabilities[8] :
d['PIN2'] = ('PIN 2 format supported', "yes")
else:
d['PIN2'] = ('PIN 2 format supported',"no")
return d
def decode_pws(self):
d = {}
if self.PW_status[0]==0:
d['ONCE'] = ('PW1 valid for several CDS', 'yes')
elif self.PW_status[0]==1:
d['ONCE'] = ('PW1 valid for several CDS', 'no')
else:
d['ONCE'] = ('PW1 valid for several CDS', 'unknown (%d)'%self.PW_status[0])
if self.PW_status[1] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[1] & 0x7f
d['PW1'] = ("PW1 format", "%s : %d bytes"%(fmt,pwlen))
if self.PW_status[2] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[2] & 0x7f
d['RC'] = ("RC format", "%s : %d bytes"%(fmt,pwlen))
if self.PW_status[3] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[3] & 0x7f
d['PW3'] = ("PW3 format", "%s : %d bytes"%(fmt,pwlen))
d['CNT1'] = ('PW1 counter', "%x"%self.PW_status[4])
d['CNTRC'] =('RC counter', "%x"%self.PW_status[5])
d['CNT3'] = ('PW3 counter', "%x"%self.PW_status[6])
return d
#slot
def select_slot(self, slot):
""" Args:
slot (int) : slot id (1 to MAX) to select
"""
self.put_data( 0x01F2, (slot-1).to_bytes(1,'big'))
#USER Info
def set_serial(self, ser):
ser=binascii.unhexlify(ser)
self.AID = self.AID[0:10]+ser
self.put_data(0x4f, self.AID[10:14])
# internals are always store as byres, get/set automatically convert from/to
def set_name(self,name):
""" Args:
name (str) : utf8 string
"""
self.name = name.encode('utf-8')
self.put_data( 0x5b, self.name)
def get_name(self):
return self.name.decode('utf-8')
def set_login(self,login):
""" Args:
login (str) : utf8 string
"""
self.login = login.encode('utf-8')
self.put_data( 0x5e, self.login)
def get_login(self):
return self.login.decode('utf-8')
def set_url(self,url):
""" Args:
url (str) : utf8 string
"""
self.url = url.encode('utf-8')
self.put_data(0x5f50, self.url)
def get_url(self):
return self.url.decode('utf-8')
def set_sex(self,sex):
""" Args:
sex (str) : ascii string ('9', '1', '2')
"""
self.sex = sex.encode('utf-8')
self.put_data(0x5f35, self.sex)
def get_sex(self):
return self.sex.decode('utf-8')
def set_lang(self,lang):
""" Args:
lang (str) : utf8 string
"""
self.lang = lang.encode('utf-8')
self.put_data(0x5f2d, self.lang)
def get_lang(self):
return self.lang.decode('utf-8')
#PINs
def verify_pin(self,id,value, pinpad=False):
""" Args:
id (int) : 0x81, 0x82, ox83
value (str) : ascii string
"""
value = value.encode('ascii')
resp,sw = self.verify(id,value, pinpad)
return sw == 0x9000
def change_pin(self, id, value,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
value = value.encode('ascii')
new_value = new_value.encode('ascii')
resp,sw = self.change_reference_data(id,value,new_value)
return sw == 0x9000
def change_RC(self,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
new_value = new_value.encode('ascii')
resp,sw = self.put_data(0xd3,new_value)
return sw == 0x9000
def reset_PW1(self,RC,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
new_value = new_value.encode('ascii')
RC = RC.encode('ascii')
resp,sw = self.reset_retry_counter(RC,new_value)
return sw == 0x9000
#keys
def get_key_uif(self,key):
"""
Returns: (int) 0,1,2,256(not supported)
"""
uif = None
if key=='sig':
uif = self.UIF_SIG
if key=='aut':
uif = self.UIF_DEC
if key=='dec':
uif = self.UIF_AUT
if uif:
uif = int.from_bytes(uif,'big')
else:
uif = 256
return uif
def get_key_fingerprints(self, key):
"""
Returns: (str) fingerprints hex string
"""
fprints = None
if key=='sig':
fprints = self.sig_fingerprints
if key=='aut':
fprints = self.aut_fingerprints
if key=='dec':
fprints = self.dec_fingerprints
if fprints:
fprints = binascii.hexlify(fprints)
else:
fprint = '-'
return fprints.decode('ascii')
def set_key_fingerprints(self, key, fprints):
fprints = binascii.unhexlify(fprints)
if key=='sig':
self.sig_fingerprints = fprints
self.put_data(0xc7, fprints)
if key=='aut':
self.aut_fingerprints = fprints
self.put_data(0xc9, fprints)
if key=='dec':
self.dec_fingerprints = fprints
self.put_data(0xc8, fprints)
def get_key_CA_fingerprints(self, key):
"""
Returns: (str) CA fingerprints hex string
"""
fprints = None
if key=='sig':
fprints = self.sig_CA_fingerprints
if key=='aut':
fprints = self.aut_CA_fingerprints
if key=='dec':
fprints = self.dec_CA_fingerprints
if fprints:
fprints = binascii.hexlify(fprints)
else:
fprint = b'-'
return fprints.decode('ascii')
def get_key_date(self, key):
"""
Returns: (str) date
"""
fdate = None
if key=='sig':
fdate = self.sig_date
if key=='aut':
fdate = self.aut_date
if key=='dec':
fdate = self.dec_date
if fdate:
fdate = datetime.datetime.fromtimestamp(int.from_bytes(fdate,'big')).isoformat(' ')
else:
fprint = b'-'.decode('ascii')
return fdate
def get_key_attribute(self, key):
"""
for RSA:
{'id': (int) 0x01,
'nsize': (int)
'esize' (int)
'format': (int)
}
for ECC:
{'id': (int) 0x18|0x19,
'OID': (bytes)
}
Args:
key: (str) 'sig' | 'aut', 'dec'
"""
attributes = None
if key=='sig':
attributes = self.sig_attribute
if key=='dec':
attributes = self.dec_attribute
if key=='aut':
attributes = self.aut_attribute
if not attributes:
return None
if len(attributes) == 0:
return None
if attributes[0] == 0x01:
return {
'id': 1,
'nsize': (attributes[1]<<8) | attributes[2],
'esize': (attributes[3]<<8) | attributes[4],
'format': attributes[5]
}
if attributes[0] == 18 or attributes[0] == 19 :
return {
'id': attributes[0] ,
'oid': attributes[1:]
}
print ("NONE: %s"%binascii.hexlify(attributes))
return None
def set_template(self, sig, dec, aut):
"""
See get_template
"""
if (sig):
self.put_data(0x00C1, binascii.unhexlify(sig))
if dec:
self.put_data(0x00C2, binascii.unhexlify(dec))
if aut:
self.put_data(0x00C3, binascii.unhexlify(aut))
pass
def asymmetric_key(self, op, key) :
"""
Args:
op: (int) 0x80 generate, 0x81 read pub, 0x82 read pub&priv
key: (str) 'sig' | 'aut', 'dec'
Returns:
for RSA:
{'id': (int) 0x01,
'n': (bytes)
'e' (bytes)
'd': (bytes)
}
for ECC:
{'id': (int) 0x18|0x19,
'OID': (bytes)
}
"""
attributes = None
if key=='sig':
attributes = self.sig_attribute
key = 0xb600
if key=='dec':
attributes = self.dec_attribute
key = 0xb800
if key=='aut':
attributes = self.aut_attribute
key = 0xa400
if not attributes:
return None
if len(attributes) == 0:
return None
resp,sw = self.generate_asym_key_pair(op,key)
if sw != 0x9000:
return None
resp,sw = self.generate_asym_key_pair(0x82,key)
tags = decode_tlv(resp)
tags = decode_tlv(tags[0x7f49])
if attributes[0] == 0x01:
return {
'id': 1,
'n': tags[0x81],
'e': tags[0x82],
'd': tags[0x98],
}

@ -1,156 +0,0 @@
# Copyright 2018 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import argparse
import binascii
from .gpgcard import GPGCard
def get_argparser():
parser = argparse.ArgumentParser(epilog="""
reset, backup, restore are always executed in THIS order.
Template identifiers are ed2559, cv25519, rsa2048, rsa3072, rsa4096.
"""
)
parser.add_argument('--adm-pin', metavar='PIN', help='Administrative PIN, if pinpad not used')
parser.add_argument('--backup', help='Perfom a full backup except the key', action='store_true')
parser.add_argument('--backup-keys', help='Perfom keys encrypted backup', action='store_true')
parser.add_argument('--file', help='basckup/restore file', type=str, default='gpg_backup')
parser.add_argument('--pinpad', help='PIN validation will be deledated to pinpad', action='store_true')
parser.add_argument('--reader', help='PCSC reader', type=str, default='pcsc:Ledger')
parser.add_argument('--reset', help='Reset the application. All data are erased', action='store_true')
parser.add_argument('--restore', help='Perfom a full restore except the key', action='store_true')
parser.add_argument('--set-serial', metavar='SERIAL', help='set the four serial bytes')
parser.add_argument('--set-templates', metavar='SIG:DEC:AUT', help='sig:dec:aut templates identifier')
parser.add_argument('--set-fingerprints', metavar='SIG:DEC:AUT', help='sig:dec:aut fingerprints, 20 bytes each in hexa')
parser.add_argument('--seed-key', help='Regenerate all keys, based on seed mode', action='store_true')
parser.add_argument('--slot', metavar='SLOT', help='slot to backup', type=int, default=1)
parser.add_argument('--user-pin', metavar='PIN', help='User PIN, if pinpad not used'),
parser.add_argument('--apdu', help='Log APDU exchange', action='store_true')
return parser
def banner():
print(
"""
GPG Ledger Admin Tool v0.1.
Copyright 2018 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
"""
)
def error(msg) :
print("Error: ")
print(" "+msg)
sys.exit()
banner()
args = get_argparser().parse_args()
if args.backup and args.restore:
error('Only one backup or restore must be specified')
if not args.pinpad:
if not args.adm_pin or not args.user_pin:
error('If pinpad is not use, userpin and admpin must be provided')
try:
print("Connect to card %s..."%args.reader, end='', flush=True)
gpgcard = GPGCard()
if args.apdu:
gpgcard.log_apdu(args.apdu)
gpgcard.connect(args.reader)
print("OK")
print("Verfify PINs...", end='', flush=True)
if args.pinpad:
if not gpgcard.verify_pin(0x82, "", True) or not gpgcard.verify_pin(0x83, "", True):
error("PIN not verified")
else:
if not gpgcard.verify_pin(0x82, args.user_pin) or not gpgcard.verify_pin(0x83, args.adm_pin):
error("PIN not verified")
print("OK")
print("Select slot %d..."%args.slot, end='', flush=True)
gpgcard.select_slot(args.slot)
print("OK")
if args.reset:
print("Reset application...", end='', flush=True)
gpgcard.terminate()
gpgcard.activate()
print("OK")
print("Get card info...", end='', flush=True)
gpgcard.get_all()
print("OK", flush=True)
if args.backup:
print("Backup application...", end='', flush=True)
if not gpgcard.backup(args.file, args.backup_keys):
error("NOK")
print("OK")
if args.restore:
print("Restore application...", end='', flush=True)
if not gpgcard.restore(args.file):
error("NOK")
print("OK", flush=True)
if args.set_templates:
print("Set template...", end='', flush=True)
templates= {
'rsa2048' : "010800002001",
'rsa3072' : "010C00002001",
'rsa4096' : "011000002001",
'nistp256' : "132A8648CE3D030107",
'ed255519' : "162B06010401DA470F01",
'cv25519' : "122B060104019755010501"
}
sig,dec,aut = args.set_templates.split(":")
gpgcard.set_template(templates[sig],templates[dec],templates[aut])
print("OK", flush=True)
if args.seed_key:
print("Seed Key...", end='', flush=True)
gpgcard.seed_key();
print("OK", flush=True)
if args.set_fingerprints:
print("Set fingerprints...", end='', flush=True)
sig,dec,aut = args.set_fingerprints.split(":")
if sig:
gpgcard.set_key_fingerprints("sig", sig)
if dec:
gpgcard.set_key_fingerprints("dec", dec)
if aut:
gpgcard.set_key_fingerprints("aut", aut)
print("OK", flush=True)
if args.set_serial:
print("Set serial...", end='', flush=True)
if len(args.set_serial) != 8 :
error('Serial must be a 4 bytes hexa string value (8 characters)')
serial = binascii.unhexlify(args.set_serial)
gpgcard.set_serial(args.set_serial)
print("OK", flush=True)
except Exception as e:
error(str(e))

@ -1,25 +0,0 @@
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from gpgcard import GPGCard
gpgcard = GPGCard()
gpgcard.connect("pcsc:Ledger")
gpgcard.get_all()
gpgcard.verify_pin(0x81, "123456")
gpgcard.verify_pin(0x83, "12345678")
gpgcard.restore("backup_card.pickle", True)

@ -0,0 +1,397 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#*****************************************************************************
# Ledger App OpenPGP.
# (c) 2024 Ledger SAS.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#*****************************************************************************
import sys
from pathlib import Path
from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
from gpgapp.gpgcard import GPGCard, GPGCardExcpetion
from gpgapp.gpgcmd import ErrorCodes, KeyTypes, PassWord
from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION
# ===============================================================================
# Parse command line options
# ===============================================================================
def get_argparser() -> Namespace:
"""Parse the commandline options"""
parser = ArgumentParser(
description="Manage OpenPGP App on Ledger device",
formatter_class=RawTextHelpFormatter
)
parser.add_argument("--info", action="store_true",
help="Get and display card information")
parser.add_argument("--reader", type=str, default="Ledger",
help="PCSC reader name (default is '%(default)s') or 'speculos'")
parser.add_argument("--apdu", action="store_true", help="Log APDU exchange")
parser.add_argument("--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)")
parser.add_argument("--reset", action="store_true",
help="Reset the application (all data will be erased)")
parser.add_argument("--pinpad", action="store_true",
help="PIN validation will be delegated to pinpad")
parser.add_argument("--adm-pin", metavar="PIN",
help="Admin PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
parser.add_argument("--user-pin", metavar="PIN",
help="User PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
parser.add_argument("--new-user-pin", metavar="PIN",
help="Change User PIN")
parser.add_argument("--new-adm-pin", metavar="PIN",
help="Change Admin PIN")
group = parser.add_mutually_exclusive_group()
group.add_argument("--reset-code", help="Update 'PW1 Resetting Code'")
group.add_argument("--reset-pw1", help="Reset the User PIN")
parser.add_argument("--serial", help="Update the 'serial' data (4 bytes)")
parser.add_argument("--salutation",choices=list(USER_SALUTATION), help="Update 'salutation' data")
parser.add_argument("--name", help="Update 'name' data")
parser.add_argument("--url", help="Update 'url' data")
parser.add_argument("--login",help="Update 'login' data")
parser.add_argument("--lang", help="Update 'lang' data")
parser.add_argument("--key-type", type=KeyTypes, choices=[k.value for k in KeyTypes],
help="Select key type SIG:DEC:AUT (default is all)")
parser.add_argument("--key-action",choices=list(KEY_OPERATIONS),
help="Generate key pair or Read public key")
parser.add_argument("--set-fingerprints", metavar="SIG:DEC:AUT",
help="Set fingerprints for selected 'key-type'\n" + \
"If 'key-type' is not specified, set for all keys (SIG:DEC:AUT)\n" + \
"Each fingerprint is 20 hex bytes long")
parser.add_argument("--set-templates", metavar="SIG:DEC:AUT",
help="Set template identifier for selected 'key-type'\n" + \
"If 'key-type' is not specified, set for all keys (SIG:DEC:AUT)\n" + \
f"Valid values are {', '.join(list(KEY_TEMPLATES))}")
parser.add_argument("--seed-key", action="store_true",
help="Regenerate all keys, based on seed mode")
parser.add_argument("--file", type=str, default="pubkey",
help="Public Key export file (default is '%(default)s')")
return parser.parse_args()
# ===============================================================================
# Error handler
# ===============================================================================
def error(code: int, msg: str) -> None:
"""Print error message and exit
Args:
msg (str): Message to display
"""
scode = f" {code:x}" if code else ""
if not msg:
if code in ErrorCodes.err_list:
msg = ErrorCodes.err_list[code]
print(f"\n### Error{scode}: {msg}\n")
sys.exit()
# ===============================================================================
# PIN codes verification
# ===============================================================================
def verify_pins(gpgcard: GPGCard, user_pin: str, adm_pin: str, pinpad: bool) -> None:
"""Verify the pin codes
Args:
gpgcard (GPGCard): smartcard object
user_pin (str): User pin code
adm_pin (str): Admin pin code
pinpad (bool): Indicates to use pinpad
"""
print("Verify PINs...")
if not gpgcard.verify_pin(PassWord.PW1, user_pin, pinpad) or \
not gpgcard.verify_pin(PassWord.PW2, user_pin, pinpad) or \
not gpgcard.verify_pin(PassWord.PW3, adm_pin, pinpad):
error(ErrorCodes.ERR_INTERNAL, "PIN not verified")
# ===============================================================================
# Reset the Application
# ===============================================================================
def reset_app(gpgcard: GPGCard) -> None:
"""Reset Application and re-init
Args:
gpgcard (GPGCard): smartcard object
"""
print("Reset application...")
gpgcard.terminate()
gpgcard.activate()
print(" -> OK")
# ===============================================================================
# Retrieve the OpenPGP Card information
# ===============================================================================
def get_info(gpgcard: GPGCard, display: bool=True) -> None:
"""Retrieve and display Card information
Args:
gpgcard (GPGCard): smartcard object
display (bool): Print Card info
"""
print("Get card info...")
gpgcard.get_all()
if not display:
return
line = "=" * 15
print(f"{line} Application Identifier {line}")
for k, v in gpgcard.decode_AID().items():
if k == "AID":
print(f" # {k:20s}: {v}")
else:
print(f" - {k:18s}: {v}")
print(f"{line} Historical Bytes {line}")
for k, v in gpgcard.decode_histo().items():
print(f" - {k:20s}: {v}")
print(f"{line} Max Extended Length {line}")
for k, v in gpgcard.decode_extlength().items():
print(f" - {k:20s}: {v}")
print(f"{line} PIN Info {line}")
for k, v in gpgcard.decode_pws().items():
print(f" - {k:20s}: {v}")
print(f"{line} Extended Capabilities {line}")
for k, v in gpgcard.decode_ext_capabilities().items():
print(f" - {k:20s}: {v}")
print(f"{line} Hardware Features {line}")
for k, v in gpgcard.decode_hardware().items():
print(f" - {k:20s}: {v}")
print(f"{line} User Info {line}")
print(f" - {'Name':20s}: {gpgcard.get_name()}")
print(f" - {'Login':20s}: {gpgcard.get_login()}")
print(f" - {'URL':20s}: {gpgcard.get_url()}")
print(f" - {'Salutation':20s}: {gpgcard.get_salutation()}")
print(f" - {'Lang':20s}: {gpgcard.get_lang()}")
print(f"{line} Slots Info {line}")
for k, v in gpgcard.decode_slot().items():
print(f" - {k:20s}: {v}")
print(f"{line} Keys Info {line}")
print(f" - {'CDS counter':20s}: {gpgcard.get_sig_count()}")
print(f" - {'RSA Pub Exponent':20s}: 0x{gpgcard.get_rsa_pub_exp():06x}")
for key in [k.value for k in KeyTypes]:
print(f" # {key}:")
print(f" - {'UIF':18s}: {gpgcard.decode_key_uif(key)}")
print(f" - {'Fingerprint':18s}: {gpgcard.get_key_fingerprint(key)}")
print(f" - {'CA fingerprint':18s}: {gpgcard.get_key_CA_fingerprint(key)}")
print(f" - {'Creation date':18s}: {gpgcard.get_key_date(key)}")
print(f" - {'Attribute':18s}: {gpgcard.decode_attributes(key)}")
print(f" - {'Certificate':18s}: {gpgcard.get_key_cert(key)}")
print(" - Key:")
for k, v in gpgcard.decode_key(key).items():
print(f" * {k:16s}: {v}")
# ===============================================================================
# Set fingerprints
# ===============================================================================
def set_fingerprints(gpgcard: GPGCard, fingerprints: str, key_type: KeyTypes | None = None) -> None:
"""Set Key template
Args:
gpgcard (GPGCard): smartcard object
fingerprints (str): SIG, DEC, AUT fingerprints separated by ':'
key_type (KeyTypes): Key type selected
"""
d = {}
if key_type is None:
# Consider all keys fingerprints are given
try:
d[KeyTypes.KEY_SIG], d[KeyTypes.KEY_DEC], d[KeyTypes.KEY_AUT] = fingerprints.split(":")
except ValueError as err:
raise GPGCardExcpetion(0, f"Wrong fingerprints arguments: {err}") from err
else:
# a key_type is specified, using only this fingerprint
d[key_type] = fingerprints
for k, v in d.items():
print(f"Set fingerprints for '{k}' Key...")
gpgcard.set_key_fingerprint(k, bytes.fromhex(v))
# ===============================================================================
# Set Key Templates
# ===============================================================================
def set_templates(gpgcard: GPGCard, templates: str, key_type: KeyTypes | None = None) -> None:
"""Set Key template
Args:
gpgcard (GPGCard): smartcard object
templates (str): SIG, DEC, AUT template separated by ':'
key_type (KeyTypes): Key type selected
"""
d = {}
if key_type is None:
# Consider all keys template are given
try:
d[KeyTypes.KEY_SIG], d[KeyTypes.KEY_DEC], d[KeyTypes.KEY_AUT] = templates.split(":")
except ValueError as err:
raise GPGCardExcpetion(0, f"Wrong templates arguments: {err}") from err
else:
# a key_type is specified, using only this template
d[key_type] = templates
for _, v in d.items():
if v not in KEY_TEMPLATES:
raise GPGCardExcpetion(0, f"Invalid template: {v}")
for k, v in d.items():
print(f"Set template {v} for '{k}' Key...")
gpgcard.set_template(k, v)
# ===============================================================================
# Handle Asymmetric keys
# ===============================================================================
def handle_key(gpgcard: GPGCard, action: str, key_type: KeyTypes, file: str = "") -> None:
"""Generate Key pair and/or Read Public key
Args:
gpgcard (GPGCard): smartcard object
action (str): Generate or Read
key_type (KeyTypes): Key type selected
file (str): Public key export file
"""
if action not in KEY_OPERATIONS:
raise GPGCardExcpetion(0, f"Invalid operation: {action}")
key_list = [key_type] if key_type else list(KeyTypes)
for key in key_list:
print(f"{action} '{key}' Key...")
key_action = "Read" if action == "Export" else action
pubkey = gpgcard.asymmetric_key(key, key_action)
if action == "Export":
if len(key_list) > 1:
filename = key + "_" + file
else:
filename = file
path = Path(filename)
if path.suffix == "":
filename += ".pem"
gpgcard.export_pub_key(pubkey, filename)
else:
for k, v in pubkey.items():
print(f" - {k:13s}: {v}")
# ===============================================================================
# MAIN
# ===============================================================================
def entrypoint() -> None:
"""Main function"""
# Arguments parsing
# -----------------
args = get_argparser()
# Arguments checking
# ------------------
if not args.pinpad:
if not args.adm_pin or not args.user_pin:
error(ErrorCodes.ERR_INTERNAL,
"If 'pinpad' is not use, 'userpin' and 'admpin' must be provided")
if args.serial and len(args.serial) != 8 :
error(ErrorCodes.ERR_INTERNAL,
"Serial must be a 4 bytes hex string value (8 characters)")
if args.reset_code and len(args.reset_code) != 8:
error(ErrorCodes.ERR_INTERNAL,
"Reset Code must be a 4 bytes hex string value (8 characters)")
if args.key_action == "Export" and not args.file:
error(ErrorCodes.ERR_INTERNAL, "Provide a file to export public key")
# Processing
# ----------
try:
print(f"Connect to card '{args.reader}'...")
gpgcard: GPGCard = GPGCard()
gpgcard.log_apdu(args.apdu)
gpgcard.connect(args.reader)
verify_pins(gpgcard, args.user_pin, args.adm_pin, args.pinpad)
if args.slot:
gpgcard.select_slot(args.slot - 1)
if args.salutation:
gpgcard.set_salutation(args.salutation)
if args.name:
gpgcard.set_name(args.name)
if args.url:
gpgcard.set_url(args.url)
if args.login:
gpgcard.set_login(args.login)
if args.lang:
gpgcard.set_lang(args.lang)
if args.new_user_pin:
gpgcard.change_pin(PassWord.PW1, args.user_pin, args.new_user_pin)
if args.new_adm_pin:
gpgcard.change_pin(PassWord.PW3, args.adm_pin, args.new_adm_pin)
if args.reset_pw1:
# Reset the User PIN with Resetting Code
gpgcard.reset_PW1(args.reset_code, args.reset_pw1)
elif args.reset_code:
# Use the Resetting code to set the value
gpgcard.set_RC(args.reset_code)
get_info(gpgcard, args.info)
if args.reset:
reset_app(gpgcard)
if args.set_templates:
set_templates(gpgcard, args.set_templates, args.key_type)
if args.seed_key:
gpgcard.seed_key()
if args.set_fingerprints:
set_fingerprints(gpgcard, args.set_fingerprints, args.key_type)
if args.serial:
gpgcard.set_serial(args.serial)
if args.key_action:
handle_key(gpgcard, args.key_action, args.key_type, args.file)
gpgcard.disconnect()
except GPGCardExcpetion as err:
error(err.code, err.message)
if __name__ == "__main__":
entrypoint()

@ -0,0 +1,2 @@
pycryptodome
ledgercomm

@ -0,0 +1,28 @@
[tool:pytest]
addopts = --strict-markers
[pylint]
disable = C0114, # missing-module-docstring
C0115, # missing-class-docstring
C0116, # missing-function-docstring
C0103, # invalid-name
C0302, # too-many-lines
R0801, # duplicate-code
R0902, # too-many-instances
R0903, # too-few-public-methods
R0904, # too-many-public-methods
R0912, # too-many-branches
R0913, # too-many-arguments
R0914, # too-many-statements
R0915 # too-many-local-variables
max-line-length=110
extension-pkg-whitelist=hid
[pycodestyle]
max-line-length = 100
[mypy-hid.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True

@ -1,76 +1,112 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_API_H
#define GPG_API_H
void USBD_CCID_activate_pinpad(int enabled);
/* ----------------------------------------------------------------------- */
/* --- INIT ---- */
/* ----------------------------------------------------------------------- */
unsigned int gpg_oid2curve(unsigned char *oid, unsigned int len);
void gpg_activate_pinpad(uint8_t enabled);
unsigned int gpg_oid2curve(unsigned char *oid, unsigned int len);
unsigned char *gpg_curve2oid(unsigned int cv, unsigned int *len);
unsigned int gpg_curve2domainlen(unsigned int cv);
unsigned int gpg_curve2domainlen(unsigned int cv);
void gpg_init(void);
void gpg_init_ux(void);
void gpg_install(unsigned char app_state);
void gpg_install_slot(gpg_key_slot_t *slot);
int gpg_dispatch(void);
int gpg_apdu_select_data(unsigned int ref, int reccord);
/* ----------------------------------------------------------------------- */
/* --- DISPATCH ---- */
/* ----------------------------------------------------------------------- */
int gpg_dispatch(void);
/* ----------------------------------------------------------------------- */
/* --- DATA ---- */
/* ----------------------------------------------------------------------- */
void gpg_apdu_select_data(unsigned int ref, int record);
int gpg_apdu_get_data(unsigned int ref);
int gpg_apdu_get_next_data(unsigned int ref);
int gpg_apdu_put_data(unsigned int ref);
int gpg_apdu_get_key_data(unsigned int ref);
int gpg_apdu_put_key_data(unsigned int ref);
void gpg_pso_derive_slot_seed(int slot, unsigned char *seed);
void gpg_pso_derive_key_seed(unsigned char *Sn,
unsigned char *key_name,
unsigned int idx,
unsigned char *Ski,
unsigned int Ski_len);
int gpg_apdu_pso(void);
int gpg_apdu_internal_authenticate(void);
int gpg_apdu_gen(void);
int gpg_apdu_get_challenge(void);
/* ----------------------------------------------------------------------- */
/* --- PSO ---- */
/* ----------------------------------------------------------------------- */
int gpg_pso_derive_slot_seed(int slot, unsigned char *seed);
int gpg_pso_derive_key_seed(unsigned char *Sn,
unsigned char *key_name,
unsigned int idx,
unsigned char *Ski,
unsigned int Ski_len);
int gpg_apdu_pso(void);
int gpg_apdu_internal_authenticate(void);
/* ----------------------------------------------------------------------- */
/* --- GEN ---- */
/* ----------------------------------------------------------------------- */
int gpg_apdu_gen(void);
/* ----------------------------------------------------------------------- */
/* --- CHALLENGE ---- */
/* ----------------------------------------------------------------------- */
int gpg_apdu_get_challenge(void);
/* ----------------------------------------------------------------------- */
/* --- SELECT ---- */
/* ----------------------------------------------------------------------- */
int gpg_apdu_select(void);
/* ----------------------------------------------------------------------- */
/* --- PIN ---- */
/* ----------------------------------------------------------------------- */
int gpg_apdu_verify(void);
int gpg_apdu_change_ref_data(void);
int gpg_apdu_reset_retry_counter(void);
gpg_pin_t *gpg_pin_get_pin(int id);
int gpg_pin_is_blocked(gpg_pin_t *pin);
int gpg_pin_is_verified(int pinID);
int gpg_pin_set_verified(int pinID, int verified);
int gpg_pin_check(gpg_pin_t *pin, int pinID, unsigned char *pin_val, unsigned int pin_len);
void gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len);
int gpg_pin_is_verified(int pinID);
void gpg_pin_set_verified(int pinID, int verified);
int gpg_pin_check(gpg_pin_t *pin, int pinID, const unsigned char *pin_val, unsigned int pin_len);
int gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len);
int gpg_mse_reset();
/* ----------------------------------------------------------------------- */
/* --- MSE ---- */
/* ----------------------------------------------------------------------- */
void gpg_mse_reset();
int gpg_apdu_mse();
/* ----------------------------------------------------------------------- */
/* --- IO ---- */
/* --- IO ---- */
/* ----------------------------------------------------------------------- */
void gpg_io_discard(int clear);
void gpg_io_clear(void);
void gpg_io_set_offset(unsigned int offset);
void gpg_io_mark(void);
void gpg_io_rewind(void);
void gpg_io_hole(unsigned int sz);
void gpg_io_inserted(unsigned int len);
void gpg_io_insert(unsigned char const *buffer, unsigned int len);
void gpg_io_insert_u32(unsigned int v32);
@ -81,36 +117,17 @@ void gpg_io_insert_t(unsigned int T);
void gpg_io_insert_tl(unsigned int T, unsigned int L);
void gpg_io_insert_tlv(unsigned int T, unsigned int L, unsigned char const *V);
void gpg_io_fetch_buffer(unsigned char *buffer, unsigned int len);
void gpg_io_fetch_buffer(unsigned char *buffer, unsigned int len);
unsigned int gpg_io_fetch_u32(void);
unsigned int gpg_io_fetch_u24(void);
unsigned int gpg_io_fetch_u16(void);
unsigned int gpg_io_fetch_u8(void);
int gpg_io_fetch_t(unsigned int *T);
int gpg_io_fetch_l(unsigned int *L);
int gpg_io_fetch_tl(unsigned int *T, unsigned int *L);
int gpg_io_fetch_nv(unsigned char *buffer, int len);
int gpg_io_fetch(unsigned char *buffer, int len);
int gpg_io_do(unsigned int io_flags);
void gpg_io_fetch_t(unsigned int *T);
void gpg_io_fetch_l(unsigned int *L);
void gpg_io_fetch_tl(unsigned int *T, unsigned int *L);
void gpg_io_fetch_nv(unsigned char *buffer, int len);
int gpg_io_fetch(unsigned char *buffer, int len);
/* ----------------------------------------------------------------------- */
/* --- TMP ---- */
/* ----------------------------------------------------------------------- */
void io_usb_ccid_set_card_inserted(unsigned int inserted);
/* ----------------------------------------------------------------------- */
/* --- DEBUG ---- */
/* ----------------------------------------------------------------------- */
#ifdef GPG_DEBUG
#error "UNSUPPORTDED: to be fixed"
#include "gpg_debug.h"
#else
#define gpg_nvm_write nvm_write
#define gpg_io_exchange io_exchange
#endif
void gpg_io_do(unsigned int io_flags);
#endif

@ -1,67 +1,95 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "cx_errors.h"
/**
* Generate a Random Number
*
* @return Status Word
*
*/
int gpg_apdu_get_challenge() {
unsigned int olen, hlen;
unsigned int olen;
cx_err_t error = CX_INTERNAL_ERROR;
unsigned char Sr[64];
if ((G_gpg_vstate.io_p1 & 0x80) == 0x80) {
olen = G_gpg_vstate.io_p2;
} else {
olen = G_gpg_vstate.io_le;
}
if (olen == 0 || olen > GPG_EXT_CHALLENGE_LENTH) {
THROW(SW_WRONG_LENGTH);
return SW_WRONG_LENGTH;
}
switch (G_gpg_vstate.io_p1) {
case CHALLENGE_NOMINAL:
case PRIME_MODE:
olen = G_gpg_vstate.io_le;
break;
case SEEDED_MODE:
olen = G_gpg_vstate.io_p2;
break;
default:
return SW_WRONG_P1P2;
}
if (olen == 0 || olen > GPG_EXT_CHALLENGE_LENTH) {
return SW_WRONG_LENGTH;
}
if ((G_gpg_vstate.io_p1 & 0x82) == 0x82) {
unsigned int path[2];
unsigned char chain[32];
unsigned char Sr[32];
if (G_gpg_vstate.io_p1 == SEEDED_MODE) {
// Ledger Add-on: Seeded random
unsigned int path[2];
unsigned char chain[32] = {0};
os_memset(chain, 0, 32);
path[0] = 0x80475047;
path[1] = 0x0F0F0F0F;
os_perso_derive_node_bip32(CX_CURVE_SECP256K1, path, 2, Sr, chain);
chain[0] = 'r';
chain[1] = 'n';
chain[2] = 'd';
explicit_bzero(chain, 32);
path[0] = 0x80475047;
path[1] = 0x0F0F0F0F;
CX_CHECK(os_derive_bip32_no_throw(CX_CURVE_SECP256K1, path, 2, Sr, chain));
chain[0] = 'r';
chain[1] = 'n';
chain[2] = 'd';
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, Sr, 32, NULL, 0);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, chain, 3, NULL, 0);
hlen = cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, G_gpg_vstate.work.io_buffer,
G_gpg_vstate.io_length, G_gpg_vstate.work.io_buffer, 32);
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, Sr, 32, NULL, 0));
CX_CHECK(
cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, chain, 3, NULL, 0));
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
CX_LAST,
G_gpg_vstate.work.io_buffer,
G_gpg_vstate.io_length,
G_gpg_vstate.work.io_buffer,
32));
CX_CHECK(cx_sha3_xof_init_no_throw(&G_gpg_vstate.work.md.sha3, 256, olen));
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha3,
CX_LAST,
G_gpg_vstate.work.io_buffer,
32,
G_gpg_vstate.work.io_buffer,
olen));
} else {
cx_rng(G_gpg_vstate.work.io_buffer, olen);
error = CX_OK;
}
cx_sha3_xof_init(&G_gpg_vstate.work.md.sha3, 256, olen);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha3, CX_LAST, G_gpg_vstate.work.io_buffer, hlen,
G_gpg_vstate.work.io_buffer, olen);
} else {
cx_rng(G_gpg_vstate.work.io_buffer, olen);
}
if (G_gpg_vstate.io_p1 == PRIME_MODE) {
// Ledger Add-on: Prime random
CX_CHECK(cx_math_next_prime_no_throw(G_gpg_vstate.work.io_buffer, olen));
}
if ((G_gpg_vstate.io_p1 & 0x81) == 0x81) {
cx_math_next_prime(G_gpg_vstate.work.io_buffer, olen);
}
gpg_io_discard(0);
gpg_io_inserted(olen);
return SW_OK;
end:
explicit_bzero(&Sr, sizeof(Sr));
if (error != CX_OK) {
return error;
}
gpg_io_discard(0);
gpg_io_inserted(olen);
return SW_OK;
}

File diff suppressed because it is too large Load Diff

@ -1,429 +1,442 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
void gpg_check_access_ins() {
unsigned int ref;
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
switch (G_gpg_vstate.io_ins) {
case INS_EXIT:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
return;
}
break;
#ifdef GPG_LOG
#warning GPG_LOG activated
case INS_GET_LOG:
return;
#endif
case INS_SELECT:
return;
case INS_GET_DATA:
case INS_GET_NEXT_DATA:
return;
case INS_VERIFY:
return;
case INS_CHANGE_REFERENCE_DATA:
return;
case INS_RESET_RETRY_COUNTER:
if (gpg_pin_is_verified(PIN_ID_PW3) || gpg_pin_is_verified(PIN_ID_RC)) {
return;
}
break;
case INS_PUT_DATA:
case INS_PUT_DATA_ODD:
return;
case INS_GEN_ASYM_KEYPAIR:
if (G_gpg_vstate.io_p1 == 0x81) {
return;
}
if (gpg_pin_is_verified(PIN_ID_PW3)) {
return;
}
break;
case INS_MSE:
return;
case INS_PSO:
if ((ref == 0x9e9a) && gpg_pin_is_verified(PIN_ID_PW1)) {
// pso:sign
if (N_gpg_pstate->PW_status[0] == 0) {
gpg_pin_set_verified(PIN_ID_PW1, 0);
}
return;
}
if (((ref == 0x8086) || (ref == 0x8680)) && gpg_pin_is_verified(PIN_ID_PW2)) {
// pso:dec/enc
return;
}
break;
case INS_INTERNAL_AUTHENTICATE:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
return;
}
break;
case INS_GET_CHALLENGE:
return;
case INS_TERMINATE_DF:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
return;
/**
* Check INS access condition
* Verify if the corresponding PW is verified
*
* @return Status Word
*
*/
static int gpg_check_access_ins() {
int sw = SW_UNKNOWN;
switch (G_gpg_vstate.io_ins) {
case INS_EXIT:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
sw = SW_OK;
}
break;
case INS_RESET_RETRY_COUNTER:
if (gpg_pin_is_verified(PIN_ID_PW3) || gpg_pin_is_verified(PIN_ID_RC)) {
sw = SW_OK;
}
break;
case INS_GEN_ASYM_KEYPAIR:
if ((G_gpg_vstate.io_p1 == ((READ_ASYM_KEY >> 8) & 0xFF)) ||
(gpg_pin_is_verified(PIN_ID_PW3))) {
sw = SW_OK;
}
break;
case INS_PSO:
if ((G_gpg_vstate.io_p1p2 == PSO_CDS) && gpg_pin_is_verified(PIN_ID_PW1)) {
// pso:sign
if (N_gpg_pstate->PW_status[0] == 0) {
gpg_pin_set_verified(PIN_ID_PW1, 0);
}
sw = SW_OK;
break;
}
if (((G_gpg_vstate.io_p1p2 == PSO_DEC) || (G_gpg_vstate.io_p1p2 == PSO_ENC)) &&
gpg_pin_is_verified(PIN_ID_PW2)) {
// pso:dec/enc
sw = SW_OK;
}
break;
case INS_INTERNAL_AUTHENTICATE:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
sw = SW_OK;
}
break;
case INS_TERMINATE_DF:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
sw = SW_OK;
}
break;
#ifdef GPG_LOG
#warning GPG_LOG activated
case INS_GET_LOG:
#endif
case INS_SELECT:
case INS_GET_DATA:
case INS_GET_NEXT_DATA:
case INS_VERIFY:
case INS_CHANGE_REFERENCE_DATA:
case INS_PUT_DATA:
case INS_PUT_DATA_ODD:
case INS_MSE:
case INS_GET_CHALLENGE:
case INS_ACTIVATE_FILE:
sw = SW_OK;
break;
default:
sw = SW_INS_NOT_SUPPORTED;
break;
}
break;
case INS_ACTIVATE_FILE:
return;
default:
THROW(SW_INS_NOT_SUPPORTED);
break;
}
THROW(SW_CONDITIONS_NOT_SATISFIED);
return sw;
}
void gpg_check_access_read_DO() {
unsigned int ref;
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
switch (ref) {
// ALWAYS
case 0x0101:
case 0x0102:
case 0x01F0:
case 0x01F1:
case 0x01F2:
case 0x01F8:
case 0x006E:
case 0x0065:
case 0x0073:
case 0x007A:
case 0x004F:
case 0x005E:
case 0x005B:
case 0x5F2D:
case 0x5F35:
case 0x5F50:
case 0x5F52:
case 0x7F21:
case 0x0093:
case 0x00C0:
case 0x00C1:
case 0x00C2:
case 0x00C3:
case 0x00C4:
case 0x00C5:
case 0x00C7:
case 0x00C8:
case 0x00C9:
case 0x00C6:
case 0x00CA:
case 0x00CD:
case 0x00CC:
case 0x00CE:
case 0x00CF:
case 0x00D0:
case 0x7F74:
case 0x7F66:
case 0x00D6:
case 0x00D7:
case 0x00D8:
return;
// PW2
case 0x0103:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
return;
}
break;
// PW3
case 0x00B6:
case 0x00A4:
case 0x00B8:
case 0x0104:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
return;
/**
* Check INS Read access condition
* Verify if the corresponding PW is verified
*
* @return Status Word
*
*/
static int gpg_check_access_read_DO() {
int sw = SW_UNKNOWN;
switch (G_gpg_vstate.io_p1p2) {
// ALWAYS
case 0x0101:
case 0x0102:
case 0x01F0:
case 0x01F1:
case 0x01F2:
case 0x01F8:
case 0x006E:
case 0x0065:
case 0x0073:
case 0x007A:
case 0x004F:
case 0x005E:
case 0x005B:
case 0x5F2D:
case 0x5F35:
case 0x5F50:
case 0x5F52:
case 0x7F21:
case 0x0093:
case 0x00C0:
case 0x00C1:
case 0x00C2:
case 0x00C3:
case 0x00C4:
case 0x00C5:
case 0x00C7:
case 0x00C8:
case 0x00C9:
case 0x00C6:
case 0x00CA:
case 0x00CD:
case 0x00CC:
case 0x00CE:
case 0x00CF:
case 0x00D0:
case 0x7F74:
case 0x7F66:
case 0x00D6:
case 0x00D7:
case 0x00D8:
sw = SW_OK;
break;
// PW2
case 0x0103:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
sw = SW_OK;
}
break;
// PW3
case 0x00B6:
case 0x00A4:
case 0x00B8:
case 0x0104:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
sw = SW_OK;
}
break;
default:
sw = SW_CONDITIONS_NOT_SATISFIED;
break;
}
break;
}
THROW(SW_CONDITIONS_NOT_SATISFIED);
return sw;
}
char debugbuff[5];
void gpg_check_access_write_DO() {
unsigned int ref;
gpg_pin_t * pin_pw2, *pin_pw3;
pin_pw2 = gpg_pin_get_pin(PIN_ID_PW2);
pin_pw3 = gpg_pin_get_pin(PIN_ID_PW3);
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
switch (ref) {
// PW2
case 0x0101:
case 0x0103:
case 0x01F2:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
return;
}
break;
// PW3
case 0x3FFF: // only used for putkey under PW3 control
case 0x4f:
case 0x0102:
case 0x0104:
case 0x01F1:
case 0x01F8:
case 0x005E:
case 0x005B:
case 0x5F2D:
case 0x5F35:
case 0x5F50:
case 0x5F48:
case 0x7F21:
case 0x00B6:
case 0x00A4:
case 0x00B8:
case 0x00C1:
case 0x00C2:
case 0x00C3:
case 0x00C4:
case 0x00C5:
case 0x00C7:
case 0x00C8:
case 0x00C9:
case 0x00C6:
case 0x00CA:
case 0x00CB:
case 0x00CC:
case 0x00CD:
case 0x00CE:
case 0x00CF:
case 0x00D0:
case 0x00D1:
case 0x00D2:
case 0x00D3:
case 0x00D5:
case 0x00F4:
case 0x00D6:
case 0x00D7:
case 0x00D8:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
return;
/**
* Check INS Write access condition
* Verify if the corresponding PW is verified
*
* @return Status Word
*
*/
static int gpg_check_access_write_DO() {
int sw = SW_UNKNOWN;
switch (G_gpg_vstate.io_p1p2) {
// PW2
case 0x0101:
case 0x0103:
case 0x01F2:
if (gpg_pin_is_verified(PIN_ID_PW2)) {
sw = SW_OK;
}
break;
// PW3
case 0x3FFF: // only used for putkey under PW3 control
case 0x4f:
case 0x0102:
case 0x0104:
case 0x01F1:
case 0x01F8:
case 0x005E:
case 0x005B:
case 0x5F2D:
case 0x5F35:
case 0x5F50:
case 0x5F48:
case 0x7F21:
case 0x00B6:
case 0x00A4:
case 0x00B8:
case 0x00C1:
case 0x00C2:
case 0x00C3:
case 0x00C4:
case 0x00C5:
case 0x00C7:
case 0x00C8:
case 0x00C9:
case 0x00C6:
case 0x00CA:
case 0x00CB:
case 0x00CC:
case 0x00CD:
case 0x00CE:
case 0x00CF:
case 0x00D0:
case 0x00D1:
case 0x00D2:
case 0x00D3:
case 0x00D5:
case 0x00F4:
case 0x00D6:
case 0x00D7:
case 0x00D8:
if (gpg_pin_is_verified(PIN_ID_PW3)) {
sw = SW_OK;
}
break;
default:
sw = SW_CONDITIONS_NOT_SATISFIED;
break;
}
break;
}
THROW(SW_CONDITIONS_NOT_SATISFIED);
return sw;
}
/* assume command is fully received */
/**
* APDU Handler: dispatch command
*
* @return Status Word
*
*/
int gpg_dispatch() {
unsigned int tag, t, l;
int sw;
if ((G_gpg_vstate.io_cla != 0x00) && (G_gpg_vstate.io_cla != 0x10) && (G_gpg_vstate.io_cla != 0xEF)) {
THROW(SW_CLA_NOT_SUPPORTED);
return SW_CLA_NOT_SUPPORTED;
}
tag = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
switch (G_gpg_vstate.io_ins) {
#ifdef GPG_LOG
case INS_GET_LOG:
gpg_io_discard(1);
gpg_io_insert(G_gpg_vstate.log_buffer, 32);
return SW_OK;
#endif
/* --- SELECT --- */
case INS_SELECT:
sw = gpg_apdu_select();
return sw;
break;
unsigned int tag, t, l;
int sw = SW_UNKNOWN;
/* --- ACTIVATE/TERMINATE FILE --- */
case INS_ACTIVATE_FILE:
gpg_io_discard(0);
if (N_gpg_pstate->histo[7] == STATE_TERMINATE) {
gpg_install(STATE_ACTIVATE);
if ((G_gpg_vstate.io_cla != CLA_APP_DEF) && (G_gpg_vstate.io_cla != CLA_APP_CHAIN) &&
(G_gpg_vstate.io_cla != CLA_APP_APDU_PIN)) {
return SW_CLA_NOT_SUPPORTED;
}
return (SW_OK);
break;
case INS_TERMINATE_DF:
gpg_io_discard(0);
if (gpg_pin_is_verified(PIN_ID_PW3) || (N_gpg_pstate->PW3.counter == 0)) {
gpg_install(STATE_TERMINATE);
return (SW_OK);
break;
}
THROW(SW_CONDITIONS_NOT_SATISFIED);
break;
}
/* Other commands allowed if not terminated */
if (N_gpg_pstate->histo[7] != 0x07) {
THROW(SW_STATE_TERMINATED);
}
/* Process */
gpg_check_access_ins();
switch (G_gpg_vstate.io_ins) {
#ifdef GPG_DEBUG_APDU
case 0x42:
sw = debug_apdu();
break;
#endif
case INS_EXIT:
os_sched_exit(0);
sw = SW_OK;
break;
/* --- CHALLENGE --- */
case INS_GET_CHALLENGE:
sw = gpg_apdu_get_challenge();
break;
/* --- DATA --- */
switch (G_gpg_vstate.io_ins) {
#ifdef GPG_LOG
case INS_GET_LOG:
gpg_io_discard(1);
gpg_io_insert(G_gpg_vstate.log_buffer, sizeof(G_gpg_vstate.log_buffer));
return SW_OK;
#endif
case INS_SELECT_DATA:
if ((G_gpg_vstate.io_p1 > 2) || (G_gpg_vstate.io_p2 != 0x04)) {
THROW(SW_WRONG_P1P2);
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x60) {
// TODO add l check
THROW(SW_DATA_INVALID);
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x5C) {
// TODO add l check
THROW(SW_WRONG_DATA);
}
if (l == 1) {
tag = gpg_io_fetch_u8();
} else if (l == 2) {
tag = gpg_io_fetch_u16();
} else {
THROW(SW_WRONG_DATA);
}
sw = gpg_apdu_select_data(tag, G_gpg_vstate.io_p1);
break;
case INS_GET_DATA:
gpg_check_access_read_DO();
switch (tag) {
case 0x00B6:
case 0x00A4:
case 0x00B8:
sw = gpg_apdu_get_key_data(tag);
break;
default:
sw = gpg_apdu_get_data(tag);
break;
}
break;
case INS_GET_NEXT_DATA:
gpg_check_access_read_DO();
sw = gpg_apdu_get_next_data(tag);
break;
case INS_PUT_DATA_ODD:
case INS_PUT_DATA:
gpg_check_access_write_DO();
switch (tag) {
case 0x00B6:
case 0x00A4:
case 0x00B8:
sw = gpg_apdu_put_key_data(tag);
break;
default:
sw = gpg_apdu_put_data(tag);
break;
/* --- SELECT --- */
case INS_SELECT:
return gpg_apdu_select();
/* --- ACTIVATE/TERMINATE FILE --- */
case INS_ACTIVATE_FILE:
gpg_io_discard(0);
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] == STATE_TERMINATE) {
gpg_install(STATE_ACTIVATE);
}
return SW_OK;
case INS_TERMINATE_DF:
gpg_io_discard(0);
if (gpg_pin_is_verified(PIN_ID_PW3) || (N_gpg_pstate->PW3.counter == 0)) {
gpg_install(STATE_TERMINATE);
return SW_OK;
}
return SW_CONDITIONS_NOT_SATISFIED;
}
break;
/* --- PIN -- */
case INS_VERIFY:
if ((G_gpg_vstate.io_p2 == 0x81) || (G_gpg_vstate.io_p2 == 0x82) || (G_gpg_vstate.io_p2 == 0x83)) {
sw = gpg_apdu_verify();
break;
/* Other commands allowed if not terminated */
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] != STATE_ACTIVATE) {
return SW_STATE_TERMINATED;
}
THROW(SW_INCORRECT_P1P2);
case INS_CHANGE_REFERENCE_DATA:
if ((G_gpg_vstate.io_p2 == 0x81) || (G_gpg_vstate.io_p2 == 0x83)) {
sw = gpg_apdu_change_ref_data();
break;
/* Process */
sw = gpg_check_access_ins();
if (sw != SW_OK) {
return sw;
}
THROW(SW_INCORRECT_P1P2);
case INS_RESET_RETRY_COUNTER:
if ((G_gpg_vstate.io_p2 == 0x81) && ((G_gpg_vstate.io_p1 == 0) || (G_gpg_vstate.io_p1 == 2))) {
sw = gpg_apdu_reset_retry_counter();
break;
switch (G_gpg_vstate.io_ins) {
case INS_EXIT:
os_sched_exit(0);
sw = SW_OK;
break;
/* --- CHALLENGE --- */
case INS_GET_CHALLENGE:
sw = gpg_apdu_get_challenge();
break;
/* --- DATA --- */
case INS_SELECT_DATA:
if ((G_gpg_vstate.io_p1 > SELECT_MAX_INSTANCE) || (G_gpg_vstate.io_p2 != SELECT_SKIP)) {
sw = SW_WRONG_P1P2;
break;
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x60) {
sw = SW_WRONG_DATA;
break;
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x5C) {
sw = SW_WRONG_DATA;
break;
}
if (l == 1) {
tag = gpg_io_fetch_u8();
} else if (l == 2) {
tag = gpg_io_fetch_u16();
} else {
sw = SW_WRONG_DATA;
break;
}
gpg_apdu_select_data(tag, G_gpg_vstate.io_p1);
sw = SW_OK;
break;
case INS_GET_DATA:
sw = gpg_check_access_read_DO();
if (sw != SW_OK) {
break;
}
switch (G_gpg_vstate.io_p1p2) {
case 0x00B6:
case 0x00A4:
case 0x00B8:
sw = gpg_apdu_get_key_data(G_gpg_vstate.io_p1p2);
break;
default:
sw = gpg_apdu_get_data(G_gpg_vstate.io_p1p2);
break;
}
break;
case INS_GET_NEXT_DATA:
sw = gpg_check_access_read_DO();
if (sw != SW_OK) {
break;
}
sw = gpg_apdu_get_next_data(G_gpg_vstate.io_p1p2);
break;
case INS_PUT_DATA_ODD:
case INS_PUT_DATA:
sw = gpg_check_access_write_DO();
if (sw != SW_OK) {
break;
}
switch (G_gpg_vstate.io_p1p2) {
case 0x00B6:
case 0x00A4:
case 0x00B8:
sw = gpg_apdu_put_key_data(G_gpg_vstate.io_p1p2);
break;
default:
sw = gpg_apdu_put_data(G_gpg_vstate.io_p1p2);
break;
}
break;
/* --- PIN -- */
case INS_VERIFY:
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) || (G_gpg_vstate.io_p2 == PIN_ID_PW2) ||
(G_gpg_vstate.io_p2 == PIN_ID_PW3)) {
sw = gpg_apdu_verify();
break;
}
sw = SW_WRONG_P1P2;
break;
case INS_CHANGE_REFERENCE_DATA:
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) || (G_gpg_vstate.io_p2 == PIN_ID_PW3)) {
sw = gpg_apdu_change_ref_data();
break;
}
sw = SW_WRONG_P1P2;
break;
case INS_RESET_RETRY_COUNTER:
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) &&
((G_gpg_vstate.io_p1 == RESET_RETRY_WITH_CODE) ||
(G_gpg_vstate.io_p1 == RESET_RETRY_WITH_PW3))) {
sw = gpg_apdu_reset_retry_counter();
break;
}
sw = SW_WRONG_P1P2;
break;
/* --- Key Management --- */
case INS_GEN_ASYM_KEYPAIR:
sw = gpg_apdu_gen();
break;
/* --- MSE --- */
case INS_MSE:
sw = gpg_apdu_mse();
break;
/* --- PSO --- */
case INS_PSO:
sw = gpg_apdu_pso();
break;
case INS_INTERNAL_AUTHENTICATE:
sw = gpg_apdu_internal_authenticate();
break;
default:
sw = SW_INS_NOT_SUPPORTED;
break;
}
THROW(SW_INCORRECT_P1P2);
/* --- Key Management --- */
case INS_GEN_ASYM_KEYPAIR:
sw = gpg_apdu_gen();
break;
/* --- MSE --- */
case INS_MSE:
sw = gpg_apdu_mse(tag);
break;
/* --- PSO --- */
case INS_PSO:
sw = gpg_apdu_pso();
break;
case INS_INTERNAL_AUTHENTICATE:
sw = gpg_apdu_internal_authenticate();
break;
default:
THROW(SW_INS_NOT_SUPPORTED);
break;
}
return sw;
return sw;
}

@ -1,284 +1,394 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "cx_ram.h"
#include "cx_errors.h"
/* @in slot slot num [0 ; GPG_KEYS_SLOTS[
* @out seed 32 bytes master seed for given slot
/**
* Derivate the App Path from the Master Seed for a specific slot
*
* @param[in] slot Selected slot
* @param[out] seed 32 bytes seed for given slot
*
* @return Status Word
*
*/
void gpg_pso_derive_slot_seed(int slot, unsigned char *seed) {
unsigned int path[2];
unsigned char chain[32];
os_memset(chain, 0, 32);
path[0] = 0x80475047;
path[1] = slot + 1;
os_perso_derive_node_bip32(CX_CURVE_SECP256K1, path, 2, seed, chain);
int gpg_pso_derive_slot_seed(int slot, unsigned char *seed) {
unsigned int path[2];
unsigned char chain[32];
cx_err_t error = CX_INTERNAL_ERROR;
explicit_bzero(chain, 32);
path[0] = 0x80475047;
path[1] = slot + 1;
CX_CHECK(os_derive_bip32_no_throw(CX_CURVE_SECP256K1, path, 2, seed, chain));
end:
if (error != CX_OK) {
return error;
}
return SW_OK;
}
/* @in Sn master seed slot number
* @in key_name key name: 'sig ', 'auth ', 'dec '
* @in idx sub-seed index
* @out Ski generated sub_seed
* @in Ski_len sub-seed length
/**
* Derivate the Key from the Generated Seed
*
* @param[in] Sn Seed for the selected slot
* @param[in] key_name key name: 'sig ', 'auth ', 'dec '
* @param[in] idx sub-seed index
* @param[out] Ski generated sub_seed
* @param[in] Ski_len sub-seed length
*
* @return Status Word
*
*/
void gpg_pso_derive_key_seed(unsigned char *Sn,
unsigned char *key_name,
unsigned int idx,
unsigned char *Ski,
unsigned int Ski_len) {
unsigned char h[32];
h[0] = idx >> 8;
h[1] = idx;
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, Sn, 32, NULL, 0);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, (unsigned char *)key_name, 4, NULL, 0);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, h, 2, h, 32);
#ifdef GPG_SHAKE256
cx_shake256_init(&G_gpg_vstate.work.md.sha3, Ski_len);
cx_sha3_update(&G_gpg_vstate.work.md.sha3, h, 32);
cx_sha3_final(&G_gpg_vstate.work.md.sha3, Ski);
#else
cx_sha3_xof_init(&G_gpg_vstate.work.md.sha3, 256, Ski_len);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha3, CX_LAST, h, 32, Ski, Ski_len);
#endif
int gpg_pso_derive_key_seed(unsigned char *Sn,
unsigned char *key_name,
unsigned int idx,
unsigned char *Ski,
unsigned int Ski_len) {
unsigned char h[32];
cx_err_t error = CX_INTERNAL_ERROR;
U2BE_ENCODE(h, 0, idx);
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, Sn, 32, NULL, 0));
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
0,
(unsigned char *) key_name,
4,
NULL,
0));
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, CX_LAST, h, 2, h, 32));
CX_CHECK(cx_shake256_init_no_throw(&G_gpg_vstate.work.md.sha3, Ski_len));
CX_CHECK(cx_sha3_update(&G_gpg_vstate.work.md.sha3, h, 32));
CX_CHECK(cx_sha3_final(&G_gpg_vstate.work.md.sha3, Ski));
end:
if (error != CX_OK) {
return error;
}
return SW_OK;
}
/* assume command is fully received */
int gpg_apdu_gen() {
unsigned int t, l, ksz, reset_cnt;
gpg_key_t * keygpg;
unsigned char seed[66];
unsigned char *name;
switch ((G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2) {
case 0x8000:
case 0x8001:
case 0x8100:
break;
default:
THROW(SW_INCORRECT_P1P2);
return SW_INCORRECT_P1P2;
}
if (G_gpg_vstate.io_lc != 2) {
THROW(SW_WRONG_LENGTH);
return SW_WRONG_LENGTH;
}
gpg_io_fetch_tl(&t, &l);
gpg_io_discard(1);
reset_cnt = 0;
switch (t) {
case 0xB6:
keygpg = &G_gpg_vstate.kslot->sig;
name = (unsigned char *)PIC("sig ");
reset_cnt = 0;
break;
case 0xA4:
keygpg = &G_gpg_vstate.kslot->aut;
name = (unsigned char *)PIC("aut ");
break;
case 0xB8:
keygpg = &G_gpg_vstate.kslot->dec;
name = (unsigned char *)PIC("dec ");
break;
default:
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
switch ((G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2) {
// -- generate keypair ---
case 0x8000:
case 0x8001:
// RSA
if (keygpg->attributes.value[0] == 0x01) {
unsigned char * pq;
cx_rsa_public_key_t * rsa_pub;
cx_rsa_private_key_t *rsa_priv, *pkey;
unsigned int pkey_size;
ksz = (keygpg->attributes.value[1] << 8) | keygpg->attributes.value[2];
ksz = ksz >> 3;
rsa_pub = (cx_rsa_public_key_t *)&G_gpg_vstate.work.rsa.public;
rsa_priv = (cx_rsa_private_key_t *)&G_gpg_vstate.work.rsa.private;
pkey = &keygpg->priv_key.rsa;
switch (ksz) {
case 1024 / 8:
pkey_size = sizeof(cx_rsa_1024_private_key_t);
break;
case 2048 / 8:
pkey_size = sizeof(cx_rsa_2048_private_key_t);
break;
case 3072 / 8:
pkey_size = sizeof(cx_rsa_3072_private_key_t);
break;
case 4096 / 8:
pkey_size = sizeof(cx_rsa_4096_private_key_t);
break;
default:
THROW(SW_WRONG_DATA);
}
pq = NULL;
if ((G_gpg_vstate.io_p2 == 0x01) || (G_gpg_vstate.seed_mode)) {
/**
* Generate a RSA key pair and writes it in NVRam
*
* @param[in] keygpg pointer on key structure
* @param[in] name key name: 'sig ', 'auth ', 'dec '
*
* @return Status Word
*
*/
static int gpg_gen_rsa_kyey(gpg_key_t *keygpg, uint8_t *name) {
cx_rsa_public_key_t *rsa_pub = NULL;
cx_rsa_private_key_t *rsa_priv = NULL;
uint8_t *pq = NULL;
uint32_t ksz = 0, reset_cnt = 0, pkey_size = 0;
int sw = SW_UNKNOWN;
cx_err_t error = CX_INTERNAL_ERROR;
uint8_t seed[66] = {0};
ksz = U2BE(keygpg->attributes.value, 1) >> 3;
rsa_pub = (cx_rsa_public_key_t *) &G_gpg_vstate.work.rsa.public;
rsa_priv = (cx_rsa_private_key_t *) &G_gpg_vstate.work.rsa.private;
switch (ksz) {
case 2048 / 8:
pkey_size = sizeof(cx_rsa_2048_private_key_t);
break;
case 3072 / 8:
pkey_size = sizeof(cx_rsa_3072_private_key_t);
break;
#ifdef WITH_SUPPORT_RSA4096
case 4096 / 8:
pkey_size = sizeof(cx_rsa_4096_private_key_t);
break;
#endif
default:
break;
}
if (pkey_size == 0) {
return SW_WRONG_DATA;
}
if ((G_gpg_vstate.io_p2 == SEEDED_MODE) || (G_gpg_vstate.seed_mode)) {
pq = &rsa_pub->n[0];
unsigned int size;
size = ksz >> 1;
gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
gpg_pso_derive_key_seed(seed, name, 1, pq, size);
gpg_pso_derive_key_seed(seed, name, 2, pq + size, size);
sw = gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
if (sw != SW_OK) {
return sw;
}
sw = gpg_pso_derive_key_seed(seed, name, 1, pq, size);
if (sw != SW_OK) {
return sw;
}
sw = gpg_pso_derive_key_seed(seed, name, 2, pq + size, size);
if (sw != SW_OK) {
return sw;
}
*pq |= 0x80;
*(pq + size) |= 0x80;
cx_math_next_prime(pq, size);
cx_math_next_prime(pq + size, size);
}
CX_CHECK(cx_math_next_prime_no_throw(pq, size));
CX_CHECK(cx_math_next_prime_no_throw(pq + size, size));
}
CX_CHECK(
cx_rsa_generate_pair_no_throw(ksz,
rsa_pub,
rsa_priv,
(const unsigned char *) N_gpg_pstate->default_RSA_exponent,
4,
pq));
nvm_write(&keygpg->priv_key.rsa, rsa_priv, pkey_size);
nvm_write(&keygpg->pub_key.rsa[0], rsa_pub->e, 4);
cx_rsa_generate_pair(ksz, rsa_pub, rsa_priv, N_gpg_pstate->default_RSA_exponent, 4, pq);
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
gpg_io_clear();
return SW_OK;
nvm_write(pkey, rsa_priv, pkey_size);
nvm_write(&keygpg->pub_key.rsa[0], rsa_pub->e, 4);
if (reset_cnt) {
reset_cnt = 0;
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
}
gpg_io_clear();
end:
return error;
}
/**
* Read a RSA key pair
*
* @param[in] keygpg pointer on key structure
*
* @return Status Word
*
*/
static int gpg_read_rsa_kyey(gpg_key_t *keygpg) {
uint32_t ksz = 0;
goto send_rsa_pub;
gpg_io_discard(1);
// check length
ksz = U2BE(keygpg->attributes.value, 1) >> 3;
gpg_io_mark();
switch (ksz) {
case 2048 / 8:
if (keygpg->priv_key.rsa2048.size == 0) {
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa2048.n);
break;
case 3072 / 8:
if (keygpg->priv_key.rsa3072.size == 0) {
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa3072.n);
break;
#ifdef WITH_SUPPORT_RSA4096
case 4096 / 8:
if (keygpg->priv_key.rsa4096.size == 0) {
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa4096.n);
break;
#endif
default:
return SW_REFERENCED_DATA_NOT_FOUND;
}
// ECC
if ((keygpg->attributes.value[0] == 18) || (keygpg->attributes.value[0] == 19) ||
(keygpg->attributes.value[0] == 22)) {
unsigned int curve, keepprivate;
keepprivate = 0;
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
if (curve == CX_CURVE_NONE) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
gpg_io_insert_tlv(0x82, 4, keygpg->pub_key.rsa);
return SW_OK;
}
/**
* Generate an Elliptic Curve key pair and writes it in NVRam
*
* @param[in] keygpg pointer on key structure
* @param[in] name key name: 'sig ', 'auth ', 'dec '
*
* @return Status Word
*
*/
static int gpg_gen_ecc_kyey(gpg_key_t *keygpg, uint8_t *name) {
uint32_t curve = 0, keepprivate = 0;
uint32_t ksz = 0, reset_cnt = 0;
int sw = SW_UNKNOWN;
cx_err_t error = CX_INTERNAL_ERROR;
uint8_t seed[66] = {0};
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
if (curve == CX_CURVE_NONE) {
return SW_REFERENCED_DATA_NOT_FOUND;
}
if ((G_gpg_vstate.io_p2 == 0x01) || (G_gpg_vstate.seed_mode)) {
ksz = gpg_curve2domainlen(curve);
gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
gpg_pso_derive_key_seed(seed, name, 1, seed, ksz);
cx_ecfp_init_private_key(curve, seed, ksz, &G_gpg_vstate.work.ecfp.private);
keepprivate = 1;
}
cx_ecfp_generate_pair(curve, &G_gpg_vstate.work.ecfp.public, &G_gpg_vstate.work.ecfp.private, keepprivate);
nvm_write(&keygpg->priv_key.ecfp, &G_gpg_vstate.work.ecfp.private, sizeof(cx_ecfp_private_key_t));
nvm_write(&keygpg->pub_key.ecfp, &G_gpg_vstate.work.ecfp.public, sizeof(cx_ecfp_public_key_t));
if (reset_cnt) {
reset_cnt = 0;
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
}
gpg_io_clear();
goto send_ecc_pub;
}
break;
// --- read pubkey ---
case 0x8100:
if (keygpg->attributes.value[0] == 0x01) {
/// read RSA
send_rsa_pub:
gpg_io_discard(1);
// check length
ksz = (keygpg->attributes.value[1] << 8) | keygpg->attributes.value[2];
ksz = ksz >> 3;
gpg_io_mark();
switch (ksz) {
case 1024 / 8:
if (keygpg->priv_key.rsa1024.size == 0) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa1024.n);
break;
case 2048 / 8:
if (keygpg->priv_key.rsa2048.size == 0) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa2048.n);
break;
case 3072 / 8:
if (keygpg->priv_key.rsa3072.size == 0) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
if ((G_gpg_vstate.io_p2 == SEEDED_MODE) || (G_gpg_vstate.seed_mode)) {
ksz = gpg_curve2domainlen(curve);
sw = gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
if (sw != SW_OK) {
return sw;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa3072.n);
break;
case 4096 / 8:
if (keygpg->priv_key.rsa4096.size == 0) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
sw = gpg_pso_derive_key_seed(seed, name, 1, seed, ksz);
if (sw != SW_OK) {
return sw;
}
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa4096.n);
break;
}
gpg_io_insert_tlv(0x82, 4, keygpg->pub_key.rsa);
CX_CHECK(
cx_ecfp_init_private_key_no_throw(curve, seed, ksz, &G_gpg_vstate.work.ecfp.private));
keepprivate = 1;
}
l = G_gpg_vstate.io_length;
gpg_io_set_offset(IO_OFFSET_MARK);
gpg_io_insert_tl(0x7f49, l);
gpg_io_set_offset(IO_OFFSET_END);
CX_CHECK(cx_ecfp_generate_pair_no_throw(curve,
&G_gpg_vstate.work.ecfp.public,
&G_gpg_vstate.work.ecfp.private,
keepprivate));
nvm_write(&keygpg->priv_key.ecfp,
&G_gpg_vstate.work.ecfp.private,
sizeof(cx_ecfp_private_key_t));
nvm_write(&keygpg->pub_key.ecfp, &G_gpg_vstate.work.ecfp.public, sizeof(cx_ecfp_public_key_t));
return SW_OK;
}
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
gpg_io_clear();
error = SW_OK;
end:
return error;
}
/**
* Read an Elliptic Curve key pair
*
* @param[in] keygpg pointer on key structure
*
* @return Status Word
*
*/
static int gpg_read_ecc_kyey(gpg_key_t *keygpg) {
uint32_t curve = 0;
uint32_t i, len;
cx_err_t error = CX_INTERNAL_ERROR;
if ((keygpg->attributes.value[0] == 18) || (keygpg->attributes.value[0] == 19) ||
(keygpg->attributes.value[0] == 22)) {
unsigned int curve;
/// read ECC
send_ecc_pub:
if (keygpg->pub_key.ecfp256.W_len == 0) {
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return 0;
}
gpg_io_discard(1);
gpg_io_mark();
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
if (curve == CX_CURVE_Ed25519) {
os_memmove(G_gpg_vstate.work.io_buffer + 128, keygpg->pub_key.ecfp256.W, keygpg->pub_key.ecfp256.W_len);
cx_edward_compress_point(CX_CURVE_Ed25519, G_gpg_vstate.work.io_buffer + 128, 65);
gpg_io_insert_tlv(0x86, 32, G_gpg_vstate.work.io_buffer + 129); // 129: discard 02
} else if (curve == CX_CURVE_Curve25519) {
unsigned int i, len;
len = keygpg->pub_key.ecfp256.W_len - 1;
if (keygpg->pub_key.ecfp.W_len == 0) {
return SW_REFERENCED_DATA_NOT_FOUND;
}
gpg_io_discard(1);
gpg_io_mark();
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
if (curve == CX_CURVE_Ed25519) {
memmove(G_gpg_vstate.work.io_buffer + 128,
keygpg->pub_key.ecfp.W,
keygpg->pub_key.ecfp.W_len);
CX_CHECK(cx_edwards_compress_point_no_throw(CX_CURVE_Ed25519,
G_gpg_vstate.work.io_buffer + 128,
65));
gpg_io_insert_tlv(0x86, 32,
G_gpg_vstate.work.io_buffer + 129); // 129: discard 02
} else if (curve == CX_CURVE_Curve25519) {
len = keygpg->pub_key.ecfp.W_len - 1;
for (i = 0; i <= len; i++) {
G_gpg_vstate.work.io_buffer[128 + i] = keygpg->pub_key.ecfp256.W[len - i];
G_gpg_vstate.work.io_buffer[128 + i] = keygpg->pub_key.ecfp.W[len - i];
}
gpg_io_insert_tlv(0x86, 32, G_gpg_vstate.work.io_buffer + 128);
} else {
gpg_io_insert_tlv(0x86, keygpg->pub_key.ecfp256.W_len, (unsigned char *)&keygpg->pub_key.ecfp256.W);
}
l = G_gpg_vstate.io_length;
gpg_io_set_offset(IO_OFFSET_MARK);
gpg_io_insert_tl(0x7f49, l);
gpg_io_set_offset(IO_OFFSET_END);
return SW_OK;
} else {
gpg_io_insert_tlv(0x86,
keygpg->pub_key.ecfp.W_len,
(unsigned char *) &keygpg->pub_key.ecfp.W);
}
break;
}
error = SW_OK;
end:
return error;
}
/**
* APDU handler to Generate/Read key pair
*
* @return Status Word
*
*/
int gpg_apdu_gen() {
uint32_t t, l;
gpg_key_t *keygpg = NULL;
uint8_t *name = NULL;
int sw = SW_UNKNOWN;
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
switch (G_gpg_vstate.io_p1p2) {
case GEN_ASYM_KEY:
case GEN_ASYM_KEY_SEED:
case READ_ASYM_KEY:
break;
default:
return SW_WRONG_P1P2;
}
if (G_gpg_vstate.io_lc != 2) {
return SW_WRONG_LENGTH;
}
gpg_io_fetch_tl(&t, &l);
gpg_io_discard(1);
switch (t) {
case KEY_SIG:
keygpg = &G_gpg_vstate.kslot->sig;
name = (unsigned char *) PIC("sig ");
break;
case KEY_AUT:
keygpg = &G_gpg_vstate.kslot->aut;
name = (unsigned char *) PIC("aut ");
break;
case KEY_DEC:
keygpg = &G_gpg_vstate.kslot->dec;
name = (unsigned char *) PIC("dec ");
break;
default:
break;
}
if (keygpg == NULL) {
return SW_WRONG_DATA;
}
switch (G_gpg_vstate.io_p1p2) {
// -- generate keypair ---
case GEN_ASYM_KEY:
case GEN_ASYM_KEY_SEED:
if (keygpg->attributes.value[0] == KEY_ID_RSA) {
sw = gpg_gen_rsa_kyey(keygpg, name);
if (sw != SW_OK) {
break;
}
} else if ((keygpg->attributes.value[0] == KEY_ID_ECDH) ||
(keygpg->attributes.value[0] == KEY_ID_ECDSA) ||
(keygpg->attributes.value[0] == KEY_ID_EDDSA)) {
sw = gpg_gen_ecc_kyey(keygpg, name);
if (sw != SW_OK) {
break;
}
}
__attribute__((fallthrough));
// --- read pubkey ---
case READ_ASYM_KEY:
if (keygpg->attributes.value[0] == KEY_ID_RSA) {
sw = gpg_read_rsa_kyey(keygpg);
} else if ((keygpg->attributes.value[0] == KEY_ID_ECDH) ||
(keygpg->attributes.value[0] == KEY_ID_ECDSA) ||
(keygpg->attributes.value[0] == KEY_ID_EDDSA)) {
sw = gpg_read_ecc_kyey(keygpg);
}
l = G_gpg_vstate.io_length;
gpg_io_set_offset(IO_OFFSET_MARK);
gpg_io_insert_tl(0x7f49, l);
gpg_io_set_offset(IO_OFFSET_END);
break;
}
return sw;
}

@ -1,183 +1,215 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "usbd_impl.h"
#include "usbd_ccid_if.h"
#include "ox_ec.h"
#define SHORT(x) ((x) >> 8) & 0xFF, (x)&0xFF
#define SHORT(x) ((x) >> 8) & 0xFF, (x) &0xFF
/* ----------------------*/
/* -- A Kind of Magic -- */
/* ----------------------*/
const unsigned char C_MAGIC[8] = {'G', 'P', 'G', 'C', 'A', 'R', 'D', '3'};
const unsigned char C_MAGIC[MAGIC_LENGTH] = {'G', 'P', 'G', 'C', 'A', 'R', 'D', '3'};
/* ----------------------*/
/* --ECC OID -- */
/* ----------------------*/
// secp256r1 / NIST P256 /ansi-x9.62 : 1.2.840.10045.3.1.7
const unsigned char C_OID_SECP256R1[8] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07};
/*
//secp384r1 / NIST P384 /ansi-x9.62 :1.3.132.0.34
/* Unsupported (yet) Curves
// secp384r1 / NIST P384 /ansi-x9.62 :1.3.132.0.34
const unsigned char C_OID_SECP384R1[5] = {
0x2B, 0x81, 0x04, 0x00 , 0x22
};
//secp521r1 / NIST P521 /ansi-x9.62 : 1.3.132.0.35
// secp521r1 / NIST P521 /ansi-x9.62 : 1.3.132.0.35
const unsigned char C_OID_SECP521R1[5] = {
0x2B, 0x81, 0x04, 0x00, 0x23
};
//secp256k1: 1.3.132.0.10
const unsigned char C_OID_SECP256K1[5] = {
0x2B, 0x81, 0x04, 0x00, 0x0A
};
*/
// secp256k1: 1.3.132.0.10
const unsigned char C_OID_SECP256K1[5] = {0x2B, 0x81, 0x04, 0x00, 0x0A};
/*
//brainpool 256t1: 1.3.36.3.3.2.8.1.1.8
/* Unsupported (yet) Curves
// brainpool 256t1: 1.3.36.3.3.2.8.1.1.8
const unsigned char C_OID_BRAINPOOL256T1[9] = {
0x2B,0x24,0x03,0x03,0x02,0x08,0x01,0x01,0x07
};
//brainpool 256r1: 1.3.36.3.3.2.8.1.1.7
// brainpool 256r1: 1.3.36.3.3.2.8.1.1.7
const unsigned char C_OID_BRAINPOOL256R1[9] = {
0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x08
};
//brainpool 284r1: 1.3.36.3.3.2.8.1.1.11
// brainpool 384r1: 1.3.36.3.3.2.8.1.1.11
const unsigned char C_OID_BRAINPOOL384R1[9] = {
0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B
};
//brainpool 512r1: 1.3.36.3.3.2.8.1.1.13
// brainpool 512r1: 1.3.36.3.3.2.8.1.1.13
const unsigned char C_OID_BRAINPOOL512R1[9] = {
0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D
};
*/
// Ed25519/curve25519: 1.3.6.1.4.1.11591.15.1
// "twisted" curve25519 for Ed25519: 1.3.6.1.4.1.11591.15.1
const unsigned char C_OID_Ed25519[9] = {
0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01,
0x2B,
0x06,
0x01,
0x04,
0x01,
0xDA,
0x47,
0x0F,
0x01,
};
// Ed25519/curve25519: 1.3.6.1.4.1.11591.15.1
// "Montgomery" curve25519 for X25519: 1.3.6.1.4.1.11591.1.5.1
const unsigned char C_OID_cv25519[10] = {
0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01,
0x2B,
0x06,
0x01,
0x04,
0x01,
0x97,
0x55,
0x01,
0x05,
0x01,
};
unsigned int gpg_oid2curve(unsigned char *oid, unsigned int len) {
if ((len == sizeof(C_OID_SECP256R1)) && (os_memcmp(oid, C_OID_SECP256R1, len) == 0)) {
return CX_CURVE_SECP256R1;
}
/*
if ( (len == sizeof(C_OID_SECP256K1)) && (os_memcmp(oid, C_OID_SECP256K1, len)==0) ) {
return CX_CURVE_SECP256K1;
}
if ( (len == sizeof(C_OID_SECP384R1)) && (os_memcmp(oid, C_OID_SECP384R1, len)==0) ) {
return CX_CURVE_SECP384R1;
}
if ( (len == sizeof(C_OID_SECP521R1)) && (os_memcmp(oid, C_OID_SECP521R1, len)==0) ) {
return CX_CURVE_SECP521R1;
}
/**
* Retrieve Curve associated to a given OID
*
* @param[in] oid Selected OID as a reference
* @param[in] len OID length
*
* @return Found Curve, or CX_CURVE_NONE if not supported
*
*/
unsigned int gpg_oid2curve(unsigned char *oid, unsigned int len) {
if ((len == sizeof(C_OID_SECP256R1)) && (memcmp(oid, C_OID_SECP256R1, len) == 0)) {
return CX_CURVE_SECP256R1;
}
/*
if ( (len == sizeof(C_OID_BRAINPOOL256R1)) && (os_memcmp(oid, C_OID_BRAINPOOL256R1, len)==0) ) {
return CX_CURVE_BrainPoolP256R1;
}
if ( (len == sizeof(C_OID_BRAINPOOL384R1)) && (os_memcmp(oid, C_OID_BRAINPOOL384R1, len)==0) ) {
return CX_CURVE_BrainPoolP384R1;
}
if ( (len == sizeof(C_OID_BRAINPOOL512R1)) && (os_memcmp(oid, C_OID_BRAINPOOL512R1, len)==0) ) {
return CX_CURVE_BrainPoolP512R1;
}
*/
if ((len == sizeof(C_OID_Ed25519)) && (os_memcmp(oid, C_OID_Ed25519, len) == 0)) {
return CX_CURVE_Ed25519;
}
if ((len == sizeof(C_OID_cv25519)) && (os_memcmp(oid, C_OID_cv25519, len) == 0)) {
return CX_CURVE_Curve25519;
}
/*
if ( (len == sizeof(C_OID_SECP256K1)) && (os_memcmp(oid, C_OID_SECP256K1, len)==0) ) {
return CX_CURVE_256K1;
}
if ( (len == sizeof(C_OID_BRAINPOOL256T1)) && (os_memcmp(oid, C_OID_BRAINPOOL256T1, len)==0) ) {
return CX_CURVE_BrainPoolP256T1;
}
*/
return CX_CURVE_NONE;
if ((len == sizeof(C_OID_SECP256K1)) && (memcmp(oid, C_OID_SECP256K1, len) == 0)) {
return CX_CURVE_SECP256K1;
}
/* Unsupported (yet) Curves
if ( (len == sizeof(C_OID_SECP384R1)) && (memcmp(oid, C_OID_SECP384R1, len)==0) ) {
return CX_CURVE_SECP384R1;
}
if ( (len == sizeof(C_OID_SECP521R1)) && (memcmp(oid, C_OID_SECP521R1, len)==0) ) {
return CX_CURVE_SECP521R1;
}
if ( (len == sizeof(C_OID_BRAINPOOL256T1)) && (memcmp(oid, C_OID_BRAINPOOL256T1, len)==0) ) {
return CX_CURVE_BrainPoolP256T1;
}
if ( (len == sizeof(C_OID_BRAINPOOL256R1)) && (memcmp(oid, C_OID_BRAINPOOL256R1, len)==0) ) {
return CX_CURVE_BrainPoolP256R1;
}
if ( (len == sizeof(C_OID_BRAINPOOL384R1)) && (memcmp(oid, C_OID_BRAINPOOL384R1, len)==0) ) {
return CX_CURVE_BrainPoolP384R1;
}
if ( (len == sizeof(C_OID_BRAINPOOL512R1)) && (memcmp(oid, C_OID_BRAINPOOL512R1, len)==0) ) {
return CX_CURVE_BrainPoolP512R1;
}
*/
if ((len == sizeof(C_OID_Ed25519)) && (memcmp(oid, C_OID_Ed25519, len) == 0)) {
return CX_CURVE_Ed25519;
}
if ((len == sizeof(C_OID_cv25519)) && (memcmp(oid, C_OID_cv25519, len) == 0)) {
return CX_CURVE_Curve25519;
}
return CX_CURVE_NONE;
}
/**
* Retrieve OID of the selected Curve
*
* @param[in] cv Selected Curve as a reference
* @param[out] len OID length
*
* @return Found OID, or NULL if not supported
*
*/
unsigned char *gpg_curve2oid(unsigned int cv, unsigned int *len) {
switch (cv) {
case CX_CURVE_SECP256R1:
*len = sizeof(C_OID_SECP256R1);
return (unsigned char *)PIC(C_OID_SECP256R1);
/*
case CX_CURVE_SECP256K1:
*len = sizeof(C_OID_SECP256K1);
return (unsigned char*)PIC(C_OID_SECP256K1);
case CX_CURVE_SECP384R1:
*len = sizeof(C_OID_SECP384R1);
return (unsigned char*)PIC(C_OID_SECP384R1);
case CX_CURVE_SECP521R1:
*len = sizeof(C_OID_SECP521R1);
return (unsigned char*)PIC(C_OID_SECP521R1);
*/
/*
case CX_CURVE_BrainPoolP256R1:
*len = sizeof(C_OID_SECP256R1);
return (unsigned char*)PIC(C_OID_SECP256R1);
case CX_CURVE_BrainPoolP384R1:
*len = sizeof(C_OID_SECP384R1);
return (unsigned char*)PIC(C_OID_SECP384R1);
case CX_CURVE_BrainPoolP512R1:
*len = sizeof(C_OID_SECP521R1);
return (unsigned char*)PIC(C_OID_SECP521R1);
*/
case CX_CURVE_Ed25519:
*len = sizeof(C_OID_Ed25519);
return (unsigned char *)PIC(C_OID_Ed25519);
case CX_CURVE_Curve25519:
*len = sizeof(C_OID_cv25519);
return (unsigned char *)PIC(C_OID_cv25519);
}
*len = 0;
return NULL;
switch (cv) {
case CX_CURVE_SECP256R1:
*len = sizeof(C_OID_SECP256R1);
return (unsigned char *) PIC(C_OID_SECP256R1);
case CX_CURVE_SECP256K1:
*len = sizeof(C_OID_SECP256K1);
return (unsigned char *) PIC(C_OID_SECP256K1);
/* Unsupported (yet) Curves
case CX_CURVE_SECP384R1:
*len = sizeof(C_OID_SECP384R1);
return (unsigned char*)PIC(C_OID_SECP384R1);
case CX_CURVE_SECP521R1:
*len = sizeof(C_OID_SECP521R1);
return (unsigned char*)PIC(C_OID_SECP521R1);
case CX_CURVE_BrainPoolP256R1:
*len = sizeof(C_OID_SECP256R1);
return (unsigned char*)PIC(C_OID_SECP256R1);
case CX_CURVE_BrainPoolP384R1:
*len = sizeof(C_OID_SECP384R1);
return (unsigned char*)PIC(C_OID_SECP384R1);
case CX_CURVE_BrainPoolP512R1:
*len = sizeof(C_OID_SECP521R1);
return (unsigned char*)PIC(C_OID_SECP521R1);
*/
case CX_CURVE_Ed25519:
*len = sizeof(C_OID_Ed25519);
return (unsigned char *) PIC(C_OID_Ed25519);
case CX_CURVE_Curve25519:
*len = sizeof(C_OID_cv25519);
return (unsigned char *) PIC(C_OID_cv25519);
}
*len = 0;
return NULL;
}
/**
* Retrieve the selected Curve length
*
* @param[in] cv Selected Curve as a reference
*
* @return Length, or 0 if not supported
*
*/
unsigned int gpg_curve2domainlen(unsigned int cv) {
switch (cv) {
case CX_CURVE_SECP256R1:
case CX_CURVE_Ed25519:
case CX_CURVE_Curve25519:
return 32;
}
return 0;
switch (cv) {
case CX_CURVE_SECP256K1:
case CX_CURVE_SECP256R1:
case CX_CURVE_Ed25519:
case CX_CURVE_Curve25519:
return 32;
}
return 0;
}
/* -------------------------------*/
@ -202,30 +234,65 @@ const unsigned char C_ext_capabilities[10] = {
};
const unsigned char C_ext_length[8] = {0x02, 0x02, SHORT(GPG_APDU_LENGTH), 0x02, 0x02, SHORT(GPG_APDU_LENGTH)};
const unsigned char C_ext_length[8] =
{0x02, 0x02, SHORT(GPG_APDU_LENGTH), 0x02, 0x02, SHORT(GPG_APDU_LENGTH)};
// General feature management
// - b8: Display (defined by ISO/IEC 7816-4)
// - b7: Biometric input sensor (defined by ISO/IEC 7816-4)
// - b6: Button
// - b5: Keypad
// - b4: LED
// - b3: Loudspeaker
// - b2: Microphone
// - b1: Touchscreen
const unsigned char C_gen_feature = 0x20;
/* ---------------------*/
/* -- default values -- */
/* ---------------------*/
const unsigned char C_default_AID[] = {0xD2, 0x76, 0x00, 0x01, 0x24, 0x01,
// version
0x03, 0x03,
// manufacturer
0x2C, 0x97,
// serial
0x00, 0x00, 0x00, 0x00,
// RFU
0x00, 0x00};
const unsigned char C_default_Histo[] = {0x00, 0x31,
0xC5, // select method: by DF/partialDF; IO-file:readbinary; RFU???
0x73,
0xC0, // select method: by DF/partialDF ,
0x01, // data coding style: ontime/byte
0x80, // chaining
0x7F, // zero state
0x90, 0x00};
const unsigned char C_default_AID[] = {
// RID: Registered application provider Identifier
0xD2,
0x76,
0x00,
0x01,
0x24,
// PIX: Proprietary application identifier extension
// application
0x01,
// version
0x03,
0x03,
// manufacturer
0x2C,
0x97,
// serial
0x00,
0x00,
0x00,
0x00,
// RFU
0x00,
0x00};
const unsigned char C_default_Histo[HISTO_LENGTH] = {
0x00,
0x31,
0xC5, // select method: by DF/partialDF; IO-file:readbinary; RFU???
0x73,
0xC0, // select method: by DF/partialDF ,
0x01, // data coding style: ontime/byte
0x80, // chaining
0x00, // Padding zero bytes
0x00,
0x00,
0x00,
0x00,
0x7F, // zero state
0x90,
0x00};
// Default template: RSA2048 010800002001 / 010800002001
#if 1
@ -233,18 +300,22 @@ const unsigned char C_default_AlgoAttr_sig[] = {
// RSA
0x01,
// Modulus default length 2048
0x08, 0x00,
0x08,
0x00,
// PubExp length 32
0x00, 0x20,
0x00,
0x20,
// std: e,p,q with modulus (n)
0x01};
const unsigned char C_default_AlgoAttr_dec[] = {
// RSA
0x01,
// Modulus default length 2048
0x08, 0x00,
0x08,
0x00,
// PubExp length 32
0x00, 0x20,
0x00,
0x20,
// std: e,p,q with modulus (n)
0x01};
#endif
@ -296,135 +367,147 @@ const unsigned char C_sha256_PW2[] = {
/* --- boot init --- */
/* ----------------------------------------------------------------------- */
/**
* App global config
*
*/
void gpg_init() {
os_memset(&G_gpg_vstate, 0, sizeof(gpg_v_state_t));
// first init ?
if (os_memcmp((void *)(N_gpg_pstate->magic), (void *)C_MAGIC, sizeof(C_MAGIC)) != 0) {
gpg_install(STATE_ACTIVATE);
gpg_nvm_write((void *)(N_gpg_pstate->magic), (void *)C_MAGIC, sizeof(C_MAGIC));
os_memset(&G_gpg_vstate, 0, sizeof(gpg_v_state_t));
}
// key conf
G_gpg_vstate.slot = N_gpg_pstate->config_slot[1];
G_gpg_vstate.kslot = &N_gpg_pstate->keys[G_gpg_vstate.slot];
gpg_mse_reset();
// pin conf
G_gpg_vstate.pinmode = N_gpg_pstate->config_pin[0];
// ux conf
gpg_init_ux();
}
explicit_bzero(&G_gpg_vstate, sizeof(gpg_v_state_t));
// first init ?
if (memcmp((void *) (N_gpg_pstate->magic), (void *) C_MAGIC, MAGIC_LENGTH) != 0) {
gpg_install(STATE_ACTIVATE);
nvm_write((void *) (N_gpg_pstate->magic), (void *) C_MAGIC, MAGIC_LENGTH);
explicit_bzero(&G_gpg_vstate, sizeof(gpg_v_state_t));
}
void gpg_init_ux() {
G_gpg_vstate.ux_type = -1;
G_gpg_vstate.ux_key = -1;
// key conf
G_gpg_vstate.slot = N_gpg_pstate->config_slot[1];
G_gpg_vstate.kslot = (gpg_key_slot_t *) &N_gpg_pstate->keys[G_gpg_vstate.slot];
gpg_mse_reset();
// pin conf
G_gpg_vstate.pinmode = N_gpg_pstate->config_pin[0];
// seed conf
G_gpg_vstate.seed_mode = 1;
// ux conf
G_gpg_vstate.ux_type = -1;
G_gpg_vstate.ux_key = -1;
}
/* ----------------------------------------------------------------------- */
/* --- Install/ReInstall GPGapp --- */
/* ----------------------------------------------------------------------- */
/**
* App dedicated slot config
*
* @param[in] slot Selected slot to configure
*
*/
void gpg_install_slot(gpg_key_slot_t *slot) {
unsigned char tmp[4];
unsigned int l;
unsigned char tmp[4];
unsigned int l;
gpg_nvm_write(slot, 0, sizeof(gpg_key_slot_t));
nvm_write(slot, 0, sizeof(gpg_key_slot_t));
cx_rng(tmp, 4);
gpg_nvm_write((void *)(slot->serial), tmp, 4);
cx_rng(tmp, 4);
nvm_write((void *) (slot->serial), tmp, 4);
l = sizeof(C_default_AlgoAttr_sig);
gpg_nvm_write((void *)(&slot->sig.attributes.value), (void *)C_default_AlgoAttr_sig, l);
gpg_nvm_write((void *)(&slot->sig.attributes.length), &l, sizeof(unsigned int));
gpg_nvm_write((void *)(&slot->aut.attributes.value), (void *)C_default_AlgoAttr_sig, l);
gpg_nvm_write((void *)(&slot->aut.attributes.length), &l, sizeof(unsigned int));
l = sizeof(C_default_AlgoAttr_sig);
nvm_write((void *) (&slot->sig.attributes.value), (void *) C_default_AlgoAttr_sig, l);
nvm_write((void *) (&slot->sig.attributes.length), &l, sizeof(unsigned int));
nvm_write((void *) (&slot->aut.attributes.value), (void *) C_default_AlgoAttr_sig, l);
nvm_write((void *) (&slot->aut.attributes.length), &l, sizeof(unsigned int));
l = sizeof(C_default_AlgoAttr_dec);
gpg_nvm_write((void *)(&slot->dec.attributes.value), (void *)C_default_AlgoAttr_dec, l);
gpg_nvm_write((void *)(&slot->dec.attributes.length), &l, sizeof(unsigned int));
l = sizeof(C_default_AlgoAttr_dec);
nvm_write((void *) (&slot->dec.attributes.value), (void *) C_default_AlgoAttr_dec, l);
nvm_write((void *) (&slot->dec.attributes.length), &l, sizeof(unsigned int));
tmp[0] = 0x00;
tmp[1] = 0x20;
gpg_nvm_write((void *)(&slot->sig.UIF), &tmp, 2);
gpg_nvm_write((void *)(&slot->dec.UIF), &tmp, 2);
gpg_nvm_write((void *)(&slot->aut.UIF), &tmp, 2);
tmp[0] = 0x00;
tmp[1] = C_gen_feature;
nvm_write((void *) (&slot->sig.UIF), &tmp, 2);
nvm_write((void *) (&slot->dec.UIF), &tmp, 2);
nvm_write((void *) (&slot->aut.UIF), &tmp, 2);
}
/**
* App 1st installation or reinstallation
*
* @param[in] app_state Current App (card) state
*
*/
void gpg_install(unsigned char app_state) {
gpg_pin_t pin;
// full reset data
gpg_nvm_write((void *)(N_gpg_pstate), NULL, sizeof(gpg_nv_state_t));
// historical bytes
os_memmove(G_gpg_vstate.work.io_buffer, C_default_Histo, sizeof(C_default_Histo));
G_gpg_vstate.work.io_buffer[7] = app_state;
gpg_nvm_write((void *)(N_gpg_pstate->histo), G_gpg_vstate.work.io_buffer, sizeof(C_default_Histo));
// AID
os_memmove(G_gpg_vstate.work.io_buffer, C_default_AID, sizeof(C_default_AID));
gpg_nvm_write((void *)(N_gpg_pstate->AID), &G_gpg_vstate.work.io_buffer, sizeof(C_default_AID));
if (app_state == STATE_ACTIVATE) {
// default sex: none
G_gpg_vstate.work.io_buffer[0] = 0x39;
gpg_nvm_write((void *)(&N_gpg_pstate->sex), G_gpg_vstate.work.io_buffer, 1);
// default PW1/PW2: 1 2 3 4 5 6
os_memmove(pin.value, C_sha256_PW1, sizeof(C_sha256_PW1));
pin.length = 6;
pin.counter = 3;
pin.ref = PIN_ID_PW1;
gpg_nvm_write((void *)(&N_gpg_pstate->PW1), &pin, sizeof(gpg_pin_t));
// default PW3: 1 2 3 4 5 6 7 8
os_memmove(pin.value, C_sha256_PW2, sizeof(C_sha256_PW2));
pin.length = 8;
pin.counter = 3;
pin.ref = PIN_ID_PW3;
gpg_nvm_write((void *)(&N_gpg_pstate->PW3), &pin, sizeof(gpg_pin_t));
// PWs status
G_gpg_vstate.work.io_buffer[0] = 1;
G_gpg_vstate.work.io_buffer[1] = GPG_MAX_PW_LENGTH;
G_gpg_vstate.work.io_buffer[2] = GPG_MAX_PW_LENGTH;
G_gpg_vstate.work.io_buffer[3] = GPG_MAX_PW_LENGTH;
gpg_nvm_write((void *)(&N_gpg_pstate->PW_status), G_gpg_vstate.work.io_buffer, 4);
// config slot
G_gpg_vstate.work.io_buffer[0] = GPG_KEYS_SLOTS;
G_gpg_vstate.work.io_buffer[1] = 0;
G_gpg_vstate.work.io_buffer[2] = 3; // 3: selection by APDU and screen
gpg_nvm_write((void *)(&N_gpg_pstate->config_slot), G_gpg_vstate.work.io_buffer, 3);
// config rsa pub
G_gpg_vstate.work.io_buffer[0] = (GPG_RSA_DEFAULT_PUB >> 24) & 0xFF;
G_gpg_vstate.work.io_buffer[1] = (GPG_RSA_DEFAULT_PUB >> 16) & 0xFF;
G_gpg_vstate.work.io_buffer[2] = (GPG_RSA_DEFAULT_PUB >> 8) & 0xFF;
G_gpg_vstate.work.io_buffer[3] = (GPG_RSA_DEFAULT_PUB >> 0) & 0xFF;
nvm_write((void *)(&N_gpg_pstate->default_RSA_exponent), G_gpg_vstate.work.io_buffer, 4);
// config pin
G_gpg_vstate.work.io_buffer[0] = PIN_MODE_CONFIRM;
gpg_nvm_write((void *)(&N_gpg_pstate->config_pin), G_gpg_vstate.work.io_buffer, 1);
USBD_CCID_activate_pinpad(3);
// default key template: RSA 2048)
for (int s = 0; s < GPG_KEYS_SLOTS; s++) {
gpg_install_slot(&N_gpg_pstate->keys[s]);
gpg_pin_t pin;
// full reset data
nvm_write((void *) (N_gpg_pstate), NULL, sizeof(gpg_nv_state_t));
// historical bytes
memmove(G_gpg_vstate.work.io_buffer, C_default_Histo, HISTO_LENGTH);
G_gpg_vstate.work.io_buffer[HISTO_OFFSET_STATE] = app_state;
nvm_write((void *) (N_gpg_pstate->histo), G_gpg_vstate.work.io_buffer, HISTO_LENGTH);
// AID
memmove(G_gpg_vstate.work.io_buffer, C_default_AID, sizeof(C_default_AID));
nvm_write((void *) (N_gpg_pstate->AID), &G_gpg_vstate.work.io_buffer, sizeof(C_default_AID));
if (app_state == STATE_ACTIVATE) {
// default salutation: none
G_gpg_vstate.work.io_buffer[0] = 0x39;
nvm_write((void *) (&N_gpg_pstate->salutation), G_gpg_vstate.work.io_buffer, 1);
// default PW1/PW2: 1 2 3 4 5 6
memmove(pin.value, C_sha256_PW1, sizeof(C_sha256_PW1));
pin.length = GPG_MIN_PW1_LENGTH;
pin.counter = 3;
pin.ref = PIN_ID_PW1;
nvm_write((void *) (&N_gpg_pstate->PW1), &pin, sizeof(gpg_pin_t));
// default PW3: 1 2 3 4 5 6 7 8
memmove(pin.value, C_sha256_PW2, sizeof(C_sha256_PW2));
pin.length = GPG_MIN_PW3_LENGTH;
pin.counter = 3;
pin.ref = PIN_ID_PW3;
nvm_write((void *) (&N_gpg_pstate->PW3), &pin, sizeof(gpg_pin_t));
// PWs status
G_gpg_vstate.work.io_buffer[0] = 1;
G_gpg_vstate.work.io_buffer[1] = GPG_MAX_PW_LENGTH;
G_gpg_vstate.work.io_buffer[2] = GPG_MAX_PW_LENGTH;
G_gpg_vstate.work.io_buffer[3] = GPG_MAX_PW_LENGTH;
nvm_write((void *) (&N_gpg_pstate->PW_status), G_gpg_vstate.work.io_buffer, 4);
// config slot
G_gpg_vstate.work.io_buffer[0] = GPG_KEYS_SLOTS;
G_gpg_vstate.work.io_buffer[1] = 0;
G_gpg_vstate.work.io_buffer[2] = 3; // 3: selection by APDU and screen
nvm_write((void *) (&N_gpg_pstate->config_slot), G_gpg_vstate.work.io_buffer, 3);
// config rsa pub
U4BE_ENCODE(G_gpg_vstate.work.io_buffer, 0, GPG_RSA_DEFAULT_PUB);
nvm_write((void *) (&N_gpg_pstate->default_RSA_exponent), G_gpg_vstate.work.io_buffer, 4);
// config pin
G_gpg_vstate.work.io_buffer[0] = PIN_MODE_CONFIRM;
nvm_write((void *) (&N_gpg_pstate->config_pin), G_gpg_vstate.work.io_buffer, 1);
gpg_activate_pinpad(3);
// default key template
for (int s = 0; s < GPG_KEYS_SLOTS; s++) {
gpg_install_slot((gpg_key_slot_t *) &N_gpg_pstate->keys[s]);
}
}
}
}
#define USBD_OFFSET_CfgDesc_bPINSupport (sizeof(USBD_CfgDesc) - 16)
void USBD_CCID_activate_pinpad(int enabled) {
#ifdef HAVE_USB_CLASS_CCID
unsigned short length;
uint8_t * cfgDesc;
unsigned char e;
e = enabled ? 3 : 0;
length = 0;
cfgDesc = USBD_GetCfgDesc_impl(&length);
nvm_write(cfgDesc + (length - 16), &e, 1);
#endif
/**
* Setup pinpad configuration
*
* @param[in] enabled pinpad configuration
*
* @return N/A
*
*/
void gpg_activate_pinpad(uint8_t enabled) {
uint8_t e = enabled ? 3 : 0;
io_usb_ccid_configure_pinpad(e);
}

@ -1,27 +1,28 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "offsets.h"
#include "ledger_assert.h"
#include "os_utils.h"
/*
* io_buff: contains current message part
* io_off: offset in current message part
* io_buffer: contains current message part
* io_offset: offset in current message part
* io_length: length of current message part
*/
@ -29,200 +30,340 @@
/* MISC */
/* ----------------------------------------------------------------------- */
/**
* Set Offset in APDU buffer
*
* @param[in] offset value to set
*
*/
void gpg_io_set_offset(unsigned int offset) {
if (offset == IO_OFFSET_END) {
G_gpg_vstate.io_offset = G_gpg_vstate.io_length;
} else if (offset == IO_OFFSET_MARK) {
G_gpg_vstate.io_offset = G_gpg_vstate.io_mark;
} else if (offset < G_gpg_vstate.io_length) {
G_gpg_vstate.io_offset = G_gpg_vstate.io_length;
} else {
THROW(ERROR_IO_OFFSET);
return;
}
switch (offset) {
case IO_OFFSET_END:
G_gpg_vstate.io_offset = G_gpg_vstate.io_length;
break;
case IO_OFFSET_MARK:
G_gpg_vstate.io_offset = G_gpg_vstate.io_mark;
break;
default:
LEDGER_ASSERT(offset < G_gpg_vstate.io_length, "Bad offset!");
G_gpg_vstate.io_offset = offset;
break;
}
}
/**
* Mark current offset in APDU buffer
*
*/
void gpg_io_mark() {
G_gpg_vstate.io_mark = G_gpg_vstate.io_offset;
G_gpg_vstate.io_mark = G_gpg_vstate.io_offset;
}
/**
* Shift empty space in APDU buffer
*
* @param[in] len space length
*
*/
void gpg_io_inserted(unsigned int len) {
G_gpg_vstate.io_offset += len;
G_gpg_vstate.io_length += len;
G_gpg_vstate.io_offset += len;
G_gpg_vstate.io_length += len;
}
/**
* Discard APDU buffer values
* Set Length, Offset and Mark to 0
*
* @param[in] clear request to fully zeroed the buffer
*
*/
void gpg_io_discard(int clear) {
G_gpg_vstate.io_length = 0;
G_gpg_vstate.io_offset = 0;
G_gpg_vstate.io_mark = 0;
if (clear) {
gpg_io_clear();
}
G_gpg_vstate.io_length = 0;
G_gpg_vstate.io_offset = 0;
G_gpg_vstate.io_mark = 0;
if (clear) {
gpg_io_clear();
}
}
/**
* Clear (zeroed) the APDU buffer
*
*/
void gpg_io_clear() {
os_memset(G_gpg_vstate.work.io_buffer, 0, GPG_IO_BUFFER_LENGTH);
explicit_bzero(G_gpg_vstate.work.io_buffer, GPG_IO_BUFFER_LENGTH);
}
/* ----------------------------------------------------------------------- */
/* INSERT data to be sent */
/* ----------------------------------------------------------------------- */
void gpg_io_hole(unsigned int sz) {
if ((G_gpg_vstate.io_length + sz) > GPG_IO_BUFFER_LENGTH) {
THROW(ERROR_IO_FULL);
return;
}
os_memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + sz,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, G_gpg_vstate.io_length - G_gpg_vstate.io_offset);
G_gpg_vstate.io_length += sz;
/**
* Move APDU buffer content after a hole
*
* @param[in] sz hole length
*
*/
static void gpg_io_hole(unsigned int sz) {
LEDGER_ASSERT((G_gpg_vstate.io_length + sz) <= GPG_IO_BUFFER_LENGTH, "Bad hole!");
memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + sz,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
G_gpg_vstate.io_length - G_gpg_vstate.io_offset);
G_gpg_vstate.io_length += sz;
}
/**
* Insert a data buffer into the APDU buffer
*
* @param[in] buff data buffer
* @param[in] len buffer length
*
*/
void gpg_io_insert(unsigned char const *buff, unsigned int len) {
gpg_io_hole(len);
os_memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, buff, len);
G_gpg_vstate.io_offset += len;
gpg_io_hole(len);
memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, buff, len);
G_gpg_vstate.io_offset += len;
}
/**
* Insert a u32 value into the APDU buffer
*
* @param[in] v32 value to insert
*
*/
void gpg_io_insert_u32(unsigned int v32) {
gpg_io_hole(4);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v32 >> 24;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] = v32 >> 16;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] = v32 >> 8;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 3] = v32 >> 0;
G_gpg_vstate.io_offset += 4;
gpg_io_hole(4);
U4BE_ENCODE(G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_offset, v32);
G_gpg_vstate.io_offset += 4;
}
/**
* Insert a u24 value into the APDU buffer
*
* @param[in] v24 value to insert
*
*/
void gpg_io_insert_u24(unsigned int v24) {
gpg_io_hole(3);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v24 >> 16;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] = v24 >> 8;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] = v24 >> 0;
G_gpg_vstate.io_offset += 3;
gpg_io_hole(3);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v24 >> 16;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] = v24 >> 8;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] = v24 >> 0;
G_gpg_vstate.io_offset += 3;
}
/**
* Insert a u16 value into the APDU buffer
*
* @param[in] v16 value to insert
*
*/
void gpg_io_insert_u16(unsigned int v16) {
gpg_io_hole(2);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v16 >> 8;
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] = v16 >> 0;
G_gpg_vstate.io_offset += 2;
gpg_io_hole(2);
U2BE_ENCODE(G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_offset, v16);
G_gpg_vstate.io_offset += 2;
}
/**
* Insert a u8 value into the APDU buffer
*
* @param[in] v8 value to insert
*
*/
void gpg_io_insert_u8(unsigned int v8) {
gpg_io_hole(1);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v8;
G_gpg_vstate.io_offset += 1;
gpg_io_hole(1);
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] = v8;
G_gpg_vstate.io_offset += 1;
}
/**
* Insert a TAG into the APDU buffer
* (To handle TLV)
*
* @param[in] T tag to insert
*
*/
void gpg_io_insert_t(unsigned int T) {
if (T & 0xFF00) {
gpg_io_insert_u16(T);
} else {
gpg_io_insert_u8(T);
}
if (T & 0xFF00) {
gpg_io_insert_u16(T);
} else {
gpg_io_insert_u8(T);
}
}
/**
* Insert a TAG/LENGTH into the APDU buffer
* (To handle TLV)
*
* @param[in] T tag to insert
* @param[in] L length to insert
*
*/
void gpg_io_insert_tl(unsigned int T, unsigned int L) {
gpg_io_insert_t(T);
if (L < 128) {
gpg_io_insert_u8(L);
} else if (L < 256) {
gpg_io_insert_u16(0x8100 | L);
} else {
gpg_io_insert_u8(0x82);
gpg_io_insert_u16(L);
}
gpg_io_insert_t(T);
if (L < 128) {
gpg_io_insert_u8(L);
} else if (L < 256) {
gpg_io_insert_u16(0x8100 | L);
} else {
gpg_io_insert_u8(0x82);
gpg_io_insert_u16(L);
}
}
/**
* Insert a TAG/LENGTH/VALUE into the APDU buffer
* (To handle TLV)
*
* @param[in] T tag to insert
* @param[in] L length to insert
* @param[in] V data to insert
*
*/
void gpg_io_insert_tlv(unsigned int T, unsigned int L, unsigned char const *V) {
gpg_io_insert_tl(T, L);
gpg_io_insert(V, L);
gpg_io_insert_tl(T, L);
gpg_io_insert(V, L);
}
/* ----------------------------------------------------------------------- */
/* FECTH data from received buffer */
/* ----------------------------------------------------------------------- */
/**
* Read a u32 value from the APDU buffer
*
* @return value retrieved
*
*/
unsigned int gpg_io_fetch_u32() {
unsigned int v32;
v32 = ((G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] << 24) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] << 16) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] << 8) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 3] << 0));
G_gpg_vstate.io_offset += 4;
return v32;
unsigned int v32;
v32 = U4BE(G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_offset);
G_gpg_vstate.io_offset += 4;
return v32;
}
/**
* Read a u24 value from the APDU buffer
*
* @return value retrieved
*
*/
unsigned int gpg_io_fetch_u24() {
unsigned int v24;
v24 = ((G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] << 16) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] << 8) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] << 0));
G_gpg_vstate.io_offset += 3;
return v24;
unsigned int v24;
v24 = ((G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] << 16) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] << 8) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2] << 0));
G_gpg_vstate.io_offset += 3;
return v24;
}
/**
* Read a u16 value from the APDU buffer
*
* @return value retrieved
*
*/
unsigned int gpg_io_fetch_u16() {
unsigned int v16;
v16 = ((G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 0] << 8) |
(G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] << 0));
G_gpg_vstate.io_offset += 2;
return v16;
unsigned int v16;
v16 = U2BE(G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_offset);
G_gpg_vstate.io_offset += 2;
return v16;
}
/**
* Read a u8 value from the APDU buffer
*
* @return value retrieved
*
*/
unsigned int gpg_io_fetch_u8() {
unsigned int v8;
v8 = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
G_gpg_vstate.io_offset += 1;
return v8;
unsigned int v8;
v8 = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
G_gpg_vstate.io_offset += 1;
return v8;
}
int gpg_io_fetch_t(unsigned int *T) {
*T = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
G_gpg_vstate.io_offset++;
if ((*T & 0x1F) == 0x1F) {
*T = (*T << 8) | G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
/**
* Read a TAG from the APDU buffer
* (To handle TLV)
*
* @param[out] T read tag
*
*/
void gpg_io_fetch_t(unsigned int *T) {
*T = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
G_gpg_vstate.io_offset++;
}
return 0;
if ((*T & 0x1F) == 0x1F) {
*T = (*T << 8) | G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
G_gpg_vstate.io_offset++;
}
}
int gpg_io_fetch_l(unsigned int *L) {
*L = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
if ((*L & 0x80) != 0) {
*L &= 0x7F;
if (*L == 1) {
*L = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1];
G_gpg_vstate.io_offset += 2;
} else if (*L == 2) {
*L = (G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1] << 8) |
G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 2];
G_gpg_vstate.io_offset += 3;
/**
* Read a LENGTH from the APDU buffer
* (To handle TLV)
*
* @param[out] L read length
*
*/
void gpg_io_fetch_l(unsigned int *L) {
*L = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset];
if ((*L & 0x80) != 0) {
*L &= 0x7F;
if (*L == 1) {
*L = G_gpg_vstate.work.io_buffer[G_gpg_vstate.io_offset + 1];
G_gpg_vstate.io_offset += 2;
} else if (*L == 2) {
*L = U2BE(G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_offset + 1);
G_gpg_vstate.io_offset += 3;
} else {
*L = -1;
}
} else {
*L = -1;
G_gpg_vstate.io_offset += 1;
}
} else {
G_gpg_vstate.io_offset += 1;
}
return 0;
}
int gpg_io_fetch_tl(unsigned int *T, unsigned int *L) {
gpg_io_fetch_t(T);
gpg_io_fetch_l(L);
return 0;
/**
* Read a TAG/LENGTH from the APDU buffer
* (To handle TLV)
*
* @param[out] T read tag
* @param[out] L read length
*
*/
void gpg_io_fetch_tl(unsigned int *T, unsigned int *L) {
gpg_io_fetch_t(T);
gpg_io_fetch_l(L);
}
int gpg_io_fetch_nv(unsigned char *buffer, int len) {
gpg_nvm_write(buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
G_gpg_vstate.io_offset += len;
return len;
/**
* Read a buffer from the APDU buffer and write it in NVRam
* (To handle TLV)
*
* @param[in] buffer NVRAM address
* @param[in] len buffer length
*
*/
void gpg_io_fetch_nv(unsigned char *buffer, int len) {
nvm_write(buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
G_gpg_vstate.io_offset += len;
}
/**
* Read a buffer from the APDU buffer
* (To handle TLV)
*
* @param[out] buffer data buffer
* @param[in] len buffer length
*
*/
int gpg_io_fetch(unsigned char *buffer, int len) {
if (buffer) {
os_memmove(buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
}
G_gpg_vstate.io_offset += len;
return len;
if (buffer) {
memmove(buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
}
G_gpg_vstate.io_offset += len;
return len;
}
/* ----------------------------------------------------------------------- */
@ -231,123 +372,147 @@ int gpg_io_fetch(unsigned char *buffer, int len) {
#define MAX_OUT GPG_APDU_LENGTH
int gpg_io_do(unsigned int io_flags) {
unsigned int rx = 0;
// if pending input chaining
if (G_gpg_vstate.io_cla & 0x10) {
goto in_chaining;
}
/**
* APDU Receive/transmit
*
* @param[in] flag io buffer flag
*
*/
void gpg_io_do(unsigned int io_flags) {
unsigned int rx = 0;
if (io_flags & IO_ASYNCH_REPLY) {
// if IO_ASYNCH_REPLY has been set,
// gpg_io_exchange will return when IO_RETURN_AFTER_TX will set in ui
rx = gpg_io_exchange(CHANNEL_APDU | IO_ASYNCH_REPLY, 0);
} else {
// --- full out chaining ---
G_gpg_vstate.io_offset = 0;
while (G_gpg_vstate.io_length > MAX_OUT) {
unsigned int tx, xx;
// send chunk
tx = MAX_OUT - 2;
os_memmove(G_io_apdu_buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, tx);
G_gpg_vstate.io_length -= tx;
G_gpg_vstate.io_offset += tx;
G_io_apdu_buffer[tx] = 0x61;
if (G_gpg_vstate.io_length > MAX_OUT - 2) {
xx = MAX_OUT - 2;
} else {
xx = G_gpg_vstate.io_length - 2;
}
G_io_apdu_buffer[tx + 1] = xx;
rx = gpg_io_exchange(CHANNEL_APDU, tx + 2);
// check get response
if ((G_io_apdu_buffer[0] != 0x00) || (G_io_apdu_buffer[1] != 0xc0) || (G_io_apdu_buffer[2] != 0x00) ||
(G_io_apdu_buffer[3] != 0x00)) {
THROW(SW_COMMAND_NOT_ALLOWED);
return 0;
}
// if pending input chaining
if (G_gpg_vstate.io_cla & CLA_APP_CHAIN) {
goto in_chaining;
}
os_memmove(G_io_apdu_buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, G_gpg_vstate.io_length);
if (io_flags & IO_RETURN_AFTER_TX) {
rx = gpg_io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_gpg_vstate.io_length);
return 0;
if (io_flags & IO_ASYNCH_REPLY) {
// if IO_ASYNCH_REPLY has been set,
// io_exchange will return when IO_RETURN_AFTER_TX will set in ui
rx = io_exchange(CHANNEL_APDU | IO_ASYNCH_REPLY, 0);
} else {
rx = gpg_io_exchange(CHANNEL_APDU, G_gpg_vstate.io_length);
}
}
//--- full in chaining ---
if (rx < 4) {
THROW(SW_COMMAND_NOT_ALLOWED);
return SW_COMMAND_NOT_ALLOWED;
}
if (rx == 4) {
G_io_apdu_buffer[4] = 0;
rx = 4;
}
G_gpg_vstate.io_offset = 0;
G_gpg_vstate.io_length = 0;
G_gpg_vstate.io_cla = G_io_apdu_buffer[0];
G_gpg_vstate.io_ins = G_io_apdu_buffer[1];
G_gpg_vstate.io_p1 = G_io_apdu_buffer[2];
G_gpg_vstate.io_p2 = G_io_apdu_buffer[3];
G_gpg_vstate.io_lc = 0;
G_gpg_vstate.io_le = 0;
switch (G_gpg_vstate.io_ins) {
case INS_GET_DATA:
case INS_GET_RESPONSE:
case INS_TERMINATE_DF:
case INS_ACTIVATE_FILE:
G_gpg_vstate.io_le = G_io_apdu_buffer[4];
break;
case INS_GET_CHALLENGE:
if (G_gpg_vstate.io_p1 == 0) {
G_gpg_vstate.io_le = G_io_apdu_buffer[4];
break;
// --- full out chaining ---
G_gpg_vstate.io_offset = 0;
while (G_gpg_vstate.io_length > MAX_OUT) {
unsigned int tx, xx;
// send chunk
tx = MAX_OUT - 2;
memmove(G_io_apdu_buffer, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, tx);
G_gpg_vstate.io_length -= tx;
G_gpg_vstate.io_offset += tx;
G_io_apdu_buffer[tx] = (SW_CORRECT_BYTES_AVAILABLE >> 8) & 0xFF;
if (G_gpg_vstate.io_length > MAX_OUT - 2) {
xx = MAX_OUT - 2;
} else {
xx = G_gpg_vstate.io_length - 2;
}
G_io_apdu_buffer[tx + 1] = xx;
io_exchange(CHANNEL_APDU, tx + 2);
// check get response APDU
if ((G_io_apdu_buffer[OFFSET_CLA] != CLA_APP_DEF) ||
(G_io_apdu_buffer[OFFSET_INS] != INS_GET_RESPONSE) ||
(G_io_apdu_buffer[OFFSET_P1] != GET_RESPONSE) ||
(G_io_apdu_buffer[OFFSET_P2] != GET_RESPONSE)) {
return;
}
}
memmove(G_io_apdu_buffer,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
G_gpg_vstate.io_length);
if (io_flags & IO_RETURN_AFTER_TX) {
io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, G_gpg_vstate.io_length);
return;
}
rx = io_exchange(CHANNEL_APDU, G_gpg_vstate.io_length);
}
case INS_VERIFY:
case INS_CHANGE_REFERENCE_DATA:
if (G_io_apdu_buffer[4] == 0) {
break;
}
goto _default;
default:
_default:
G_gpg_vstate.io_lc = G_io_apdu_buffer[4];
os_memmove(G_gpg_vstate.work.io_buffer, G_io_apdu_buffer + 5, G_gpg_vstate.io_lc);
G_gpg_vstate.io_length = G_gpg_vstate.io_lc;
break;
}
while (G_gpg_vstate.io_cla & 0x10) {
G_io_apdu_buffer[0] = 0x90;
G_io_apdu_buffer[1] = 0x00;
rx = gpg_io_exchange(CHANNEL_APDU, 2);
in_chaining:
if ((rx < 4) || ((G_io_apdu_buffer[0] & 0xEF) != (G_gpg_vstate.io_cla & 0xEF)) ||
(G_io_apdu_buffer[1] != G_gpg_vstate.io_ins) || (G_io_apdu_buffer[2] != G_gpg_vstate.io_p1) ||
(G_io_apdu_buffer[3] != G_gpg_vstate.io_p2)) {
THROW(SW_COMMAND_NOT_ALLOWED);
return SW_COMMAND_NOT_ALLOWED;
//--- full in chaining ---
if (rx < 4) {
return;
}
if (rx == 4) {
G_io_apdu_buffer[4] = 0;
rx = 4;
G_io_apdu_buffer[OFFSET_LC] = 0;
}
G_gpg_vstate.io_cla = G_io_apdu_buffer[0];
G_gpg_vstate.io_lc = G_io_apdu_buffer[4];
if ((G_gpg_vstate.io_length + G_gpg_vstate.io_lc) > GPG_IO_BUFFER_LENGTH) {
return 1;
G_gpg_vstate.io_offset = 0;
G_gpg_vstate.io_length = 0;
G_gpg_vstate.io_cla = G_io_apdu_buffer[OFFSET_CLA];
G_gpg_vstate.io_ins = G_io_apdu_buffer[OFFSET_INS];
G_gpg_vstate.io_p1 = G_io_apdu_buffer[OFFSET_P1];
G_gpg_vstate.io_p2 = G_io_apdu_buffer[OFFSET_P2];
G_gpg_vstate.io_lc = 0;
G_gpg_vstate.io_le = 0;
G_gpg_vstate.io_p1p2 = U2(G_gpg_vstate.io_p1, G_gpg_vstate.io_p2);
switch (G_gpg_vstate.io_ins) {
case INS_GET_DATA:
case INS_GET_RESPONSE:
case INS_TERMINATE_DF:
case INS_ACTIVATE_FILE:
G_gpg_vstate.io_le = G_io_apdu_buffer[OFFSET_LC];
break;
case INS_GET_CHALLENGE:
if (G_gpg_vstate.io_p1 == CHALLENGE_NOMINAL) {
G_gpg_vstate.io_le = G_io_apdu_buffer[OFFSET_LC];
break;
}
__attribute__((fallthrough));
case INS_VERIFY:
case INS_CHANGE_REFERENCE_DATA:
if (G_io_apdu_buffer[OFFSET_LC] == 0) {
break;
}
__attribute__((fallthrough));
default:
G_gpg_vstate.io_lc = G_io_apdu_buffer[OFFSET_LC];
memmove(G_gpg_vstate.work.io_buffer,
G_io_apdu_buffer + OFFSET_CDATA,
G_gpg_vstate.io_lc);
G_gpg_vstate.io_length = G_gpg_vstate.io_lc;
break;
}
os_memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_length, G_io_apdu_buffer + 5, G_gpg_vstate.io_lc);
G_gpg_vstate.io_length += G_gpg_vstate.io_lc;
}
return 0;
PRINTF("[IO] - io_do: 1st APDU=0x %02x.%02x.%02x.%02x - %d (0x%x)\n",
G_gpg_vstate.io_cla,
G_gpg_vstate.io_ins,
G_gpg_vstate.io_p1,
G_gpg_vstate.io_p2,
G_gpg_vstate.io_lc,
G_gpg_vstate.io_lc);
while (G_gpg_vstate.io_cla & CLA_APP_CHAIN) {
G_io_apdu_buffer[0] = ((SW_OK >> 8) & 0xFF);
G_io_apdu_buffer[1] = (SW_OK & 0xFF);
rx = io_exchange(CHANNEL_APDU, 2);
in_chaining:
if ((rx < 4) ||
((G_io_apdu_buffer[OFFSET_CLA] & CLA_APP_APDU_PIN) !=
(G_gpg_vstate.io_cla & CLA_APP_APDU_PIN)) ||
(G_io_apdu_buffer[OFFSET_INS] != G_gpg_vstate.io_ins) ||
(G_io_apdu_buffer[OFFSET_P1] != G_gpg_vstate.io_p1) ||
(G_io_apdu_buffer[OFFSET_P2] != G_gpg_vstate.io_p2)) {
return;
}
if (rx == 4) {
G_io_apdu_buffer[OFFSET_LC] = 0;
}
G_gpg_vstate.io_cla = G_io_apdu_buffer[OFFSET_CLA];
G_gpg_vstate.io_lc = G_io_apdu_buffer[OFFSET_LC];
if ((G_gpg_vstate.io_length + G_gpg_vstate.io_lc) > GPG_IO_BUFFER_LENGTH) {
return;
}
PRINTF("[IO] - io_do: Next APDU=0x %02x.%02x.%02x.%02x - %d (0x%x)\n",
G_gpg_vstate.io_cla,
G_gpg_vstate.io_ins,
G_gpg_vstate.io_p1,
G_gpg_vstate.io_p2,
G_gpg_vstate.io_lc,
G_gpg_vstate.io_lc);
memmove(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_length,
G_io_apdu_buffer + OFFSET_CDATA,
G_gpg_vstate.io_lc);
G_gpg_vstate.io_length += G_gpg_vstate.io_lc;
}
}

@ -1,192 +1,62 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_DEBUG_MAIN
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "gpg_ux_nanos.h"
//#include "gpg_ux_blue.h"
#include "os_io_seproxyhal.h"
#include "string.h"
#include "glyphs.h"
#include "gpg_ux.h"
#include "io.h"
#include "usbd_ccid_if.h"
/* ----------------------------------------------------------------------- */
/* --- Application Entry --- */
/* ----------------------------------------------------------------------- */
void gpg_main(void) {
unsigned int io_flags;
io_flags = 0;
for (;;) {
volatile unsigned short sw = 0;
BEGIN_TRY {
TRY {
gpg_io_do(io_flags);
sw = gpg_dispatch();
}
CATCH_OTHER(e) {
gpg_io_discard(1);
if ((e & 0xFFFF0000) || (((e & 0xF000) != 0x6000) && ((e & 0xF000) != 0x9000))) {
gpg_io_insert_u32(e);
sw = 0x6f42;
} else {
sw = e;
}
}
FINALLY {
if (sw) {
gpg_io_insert_u16(sw);
io_flags = 0;
} else {
io_flags = IO_ASYNCH_REPLY;
}
}
}
END_TRY;
}
}
unsigned char io_event(unsigned char channel) {
// nothing done with the event, throw an error on the transport layer if
// needed
// can't have more than one tag in the reply, not supported yet.
switch (G_io_seproxyhal_spi_buffer[0]) {
case SEPROXYHAL_TAG_FINGER_EVENT:
UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer);
break;
// power off if long push, else pass to the application callback if any
case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S
UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer);
break;
// other events are propagated to the UX just in case
default:
UX_DEFAULT_EVENT();
break;
case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT:
UX_DISPLAYED_EVENT({});
break;
case SEPROXYHAL_TAG_TICKER_EVENT:
UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, {
// only allow display when not locked of overlayed by an OS UX.
if (UX_ALLOWED) {
UX_REDISPLAY();
}
});
break;
}
// close the event if not done previously (by a display or whatever)
if (!io_seproxyhal_spi_is_status_sent()) {
io_seproxyhal_general_status();
}
// command has been processed, DO NOT reset the current APDU transport
return 1;
}
unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) {
switch (channel & ~(IO_FLAGS)) {
case CHANNEL_KEYBOARD:
break;
void app_main(void) {
unsigned int io_flags = 0;
io_flags = 0;
volatile unsigned short sw = SW_UNKNOWN;
// multiplexed io exchange over a SPI channel and TLV encapsulated protocol
case CHANNEL_SPI:
if (tx_len) {
io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len);
// start communication with MCU
ui_CCID_reset();
if (channel & IO_RESET_AFTER_REPLIED) {
reset();
}
return 0; // nothing received from the master so far (it's a tx
// transaction)
} else {
return io_seproxyhal_spi_recv(G_io_apdu_buffer, sizeof(G_io_apdu_buffer), 0);
}
default:
THROW(INVALID_PARAMETER);
return 0;
}
return 0;
}
void app_exit(void) {
BEGIN_TRY_L(exit) {
TRY_L(exit) {
os_sched_exit(-1);
}
FINALLY_L(exit) {
}
}
END_TRY_L(exit);
}
/* -------------------------------------------------------------- */
__attribute__((section(".boot"))) int main(void) {
// exit critical section
__asm volatile("cpsie i");
// set up
io_init();
// ensure exception will work as planned
os_boot();
for (;;) {
UX_INIT();
gpg_init();
BEGIN_TRY {
TRY {
// start communication with MCU
io_seproxyhal_init();
// set up initial screen
ui_init();
USB_power(1);
#if HAVE_USB_CLASS_CCID
io_usb_ccid_set_card_inserted(1);
#endif
// set up
gpg_init();
// set up initial screen
ui_init();
// start the application
// the first exchange will:
// - display the initial screen
// - send the ATR
// - receive the first command
gpg_main();
}
CATCH(EXCEPTION_IO_RESET) {
// reset IO and UX
continue;
}
CATCH_ALL {
break;
}
FINALLY {
}
// start the application
// the first exchange will:
// - display the initial screen
// - send the ATR
// - receive the first command
for (;;) {
gpg_io_do(io_flags);
sw = gpg_dispatch();
if (sw) {
PRINTF("[MAIN] - FINALLY INSERT sw=0x%x\n", sw);
if ((sw != SW_OK) && ((sw & 0xFF00) != SW_CORRECT_BYTES_AVAILABLE)) {
gpg_io_discard(1);
}
gpg_io_insert_u16(sw);
io_flags = 0;
} else {
io_flags = IO_ASYNCH_REPLY;
}
}
END_TRY;
}
app_exit();
}
#endif

@ -1,72 +1,83 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
static int gpg_mse_set(int crt, int ref) {
if (crt == 0xA4) {
if (ref == 0x02) {
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->dec;
}
if (ref == 0x03) {
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->aut;
/**
* Set a new MSE configuration
*
* @param[in] crt selected key
* @param[in] ref new operation type
*
*/
static void gpg_mse_set(int crt, int ref) {
if (crt == KEY_AUT) {
if (ref == 0x02) {
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->dec;
}
if (ref == 0x03) {
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->aut;
}
}
}
if (crt == 0xB8) {
if (ref == 0x02) {
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->dec;
}
if (ref == 0x03) {
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->aut;
if (crt == KEY_DEC) {
if (ref == 0x02) {
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->dec;
}
if (ref == 0x03) {
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->aut;
}
}
}
return 0;
}
int gpg_mse_reset() {
gpg_mse_set(0xA4, 0x03);
gpg_mse_set(0xB8, 0x02);
return 0;
/**
* Reset MSE config
*
*/
void gpg_mse_reset() {
gpg_mse_set(KEY_AUT, 0x03);
gpg_mse_set(KEY_DEC, 0x02);
}
/**
* APDU handler to Manage Security Environment
*
* @return Status Word
*
*/
int gpg_apdu_mse() {
int crt, ref;
int crt, ref;
if ((G_gpg_vstate.io_p1 != 0x41) || ((G_gpg_vstate.io_p2 != 0xA4) && (G_gpg_vstate.io_p2 != 0xB8))) {
THROW(SW_INCORRECT_P1P2);
return SW_INCORRECT_P1P2;
}
if ((G_gpg_vstate.io_p1 != MSE_SET) ||
((G_gpg_vstate.io_p2 != KEY_AUT) && (G_gpg_vstate.io_p2 != KEY_DEC))) {
return SW_WRONG_P1P2;
}
crt = gpg_io_fetch_u16();
if (crt != 0x8301) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
crt = gpg_io_fetch_u16();
if (crt != 0x8301) {
return SW_WRONG_DATA;
}
ref = gpg_io_fetch_u8();
if ((ref != 0x02) && (ref != 0x03)) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
ref = gpg_io_fetch_u8();
if ((ref != 0x02) && (ref != 0x03)) {
return SW_WRONG_DATA;
}
gpg_mse_set(crt, ref);
gpg_io_discard(1);
return SW_OK;
}
gpg_mse_set(G_gpg_vstate.io_p2, ref);
gpg_io_discard(1);
return SW_OK;
}

@ -1,26 +0,0 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
const gpg_nv_state_t N_state_pic;
#else
gpg_nv_state_t N_state_pic;
#endif

@ -1,280 +1,365 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "gpg_ux.h"
#include "gpg_ux_nanos.h"
/**
* Get Pin structure from reference ID value
*
* @param[in] pinref PinCode reference ID
*
* @return Pin structure, or NULL if invalid
*
*/
gpg_pin_t *gpg_pin_get_pin(int pinref) {
switch (pinref) {
case PIN_ID_PW1:
case PIN_ID_PW2:
return &N_gpg_pstate->PW1;
case PIN_ID_PW3:
return &N_gpg_pstate->PW3;
case PIN_ID_RC:
return &N_gpg_pstate->RC;
}
return NULL;
switch (pinref) {
case PIN_ID_PW1:
case PIN_ID_PW2:
return (gpg_pin_t *) &N_gpg_pstate->PW1;
case PIN_ID_PW3:
return (gpg_pin_t *) &N_gpg_pstate->PW3;
case PIN_ID_RC:
return (gpg_pin_t *) &N_gpg_pstate->RC;
}
return NULL;
}
/**
* Get Pin index from reference ID value
*
* @param[in] pinref PinCode reference ID
*
* @return Pin index
*
*/
static int gpg_pin_get_state_index(unsigned int pinref) {
switch (pinref) {
case PIN_ID_PW1:
return 1;
case PIN_ID_PW2:
return 2;
case PIN_ID_PW3:
return 3;
case PIN_ID_RC:
return 4;
}
return -1;
}
static int gpg_pin_check_internal(gpg_pin_t *pin, unsigned char *pin_val, int pin_len) {
unsigned int counter;
if (pin->counter == 0) {
return SW_PIN_BLOCKED;
}
counter = pin->counter - 1;
gpg_nvm_write(&(pin->counter), &counter, sizeof(int));
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, pin_val, pin_len, G_gpg_vstate.work.md.H,
sizeof(G_gpg_vstate.work.md.H));
if (os_memcmp(G_gpg_vstate.work.md.H, pin->value, 32)) {
return SW_SECURITY_STATUS_NOT_SATISFIED;
}
counter = 3;
gpg_nvm_write(&(pin->counter), &counter, sizeof(int));
return SW_OK;
switch (pinref) {
case PIN_ID_PW1:
return 1;
case PIN_ID_PW2:
return 2;
case PIN_ID_PW3:
return 3;
case PIN_ID_RC:
return 4;
}
return -1;
}
static void gpg_pin_check_throw(gpg_pin_t *pin, int pinID, unsigned char *pin_val, int pin_len) {
int sw;
gpg_pin_set_verified(pinID, 0);
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
if (sw == SW_OK) {
gpg_pin_set_verified(pinID, 1);
return;
}
THROW(sw);
return;
}
/**
* Compare the PinCode hash and handle the associated counter
*
* @param[in] pin PinCode reference to check
* @param[in] pin_val PinCode value
* @param[in] pin_len PinCode length
*
* @return Status Word
*
*/
static int gpg_pin_check_internal(gpg_pin_t *pin, const unsigned char *pin_val, int pin_len) {
unsigned int counter;
cx_err_t error = CX_INTERNAL_ERROR;
int gpg_pin_check(gpg_pin_t *pin, int pinID, unsigned char *pin_val, unsigned int pin_len) {
int sw;
gpg_pin_set_verified(pinID, 0);
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
if (sw == SW_OK) {
gpg_pin_set_verified(pinID, 1);
}
return sw;
}
if (pin->counter == 0) {
return SW_PIN_BLOCKED;
}
void gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len) {
cx_sha256_t sha256;
counter = pin->counter - 1;
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
CX_LAST,
pin_val,
pin_len,
G_gpg_vstate.work.md.H,
sizeof(G_gpg_vstate.work.md.H)));
if (memcmp(G_gpg_vstate.work.md.H, pin->value, 32)) {
error = (counter == 0) ? SW_PIN_BLOCKED : SW_SECURITY_STATUS_NOT_SATISFIED;
} else {
counter = 3;
error = SW_OK;
}
gpg_pin_t newpin;
end:
if (counter != pin->counter) {
nvm_write(&(pin->counter), &counter, sizeof(int));
}
return error;
}
cx_sha256_init(&sha256);
cx_hash((cx_hash_t *)&sha256, CX_LAST, pin_val, pin_len, newpin.value, 32);
newpin.length = pin_len;
newpin.counter = 3;
/**
* Check the PinCode value and set verification status
*
* @param[in] pin PinCode reference to check
* @param[in] pin_val PinCode value
* @param[in] pin_len PinCode length
*
* @return Status Word
*
*/
int gpg_pin_check(gpg_pin_t *pin, int pinID, const unsigned char *pin_val, unsigned int pin_len) {
int sw = SW_UNKNOWN;
gpg_pin_set_verified(pinID, 0);
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
if (sw == SW_OK) {
gpg_pin_set_verified(pinID, 1);
}
return sw;
}
gpg_nvm_write(pin, &newpin, sizeof(gpg_pin_t));
/**
* Set the PinCode value in NVRam
*
* @param[in] pin PinCode reference to set
* @param[in] pin_val PinCode value
* @param[in] pin_len PinCode length
*
* @return Status Word
*
*/
int gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len) {
cx_sha256_t sha256;
cx_err_t error = CX_INTERNAL_ERROR;
gpg_pin_t newpin;
cx_sha256_init(&sha256);
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &sha256, CX_LAST, pin_val, pin_len, newpin.value, 32));
newpin.length = pin_len;
newpin.counter = 3;
nvm_write(pin, &newpin, sizeof(gpg_pin_t));
end:
if (error != CX_OK) {
return error;
}
return SW_OK;
}
int gpg_pin_set_verified(int pinID, int verified) {
int idx;
idx = gpg_pin_get_state_index(pinID);
if (idx >= 0) {
G_gpg_vstate.verified_pin[idx] = verified;
return verified;
}
return 0;
/**
* Change the Pin verification status
*
* @param[in] pinID PinCode ID to change
* @param[in] verified new status
*
*/
void gpg_pin_set_verified(int pinID, int verified) {
int idx;
idx = gpg_pin_get_state_index(pinID);
if (idx >= 0) {
G_gpg_vstate.verified_pin[idx] = verified;
}
}
/**
* Check if the selected Pin is verified
*
* @param[in] pinID PinCode ID to check
*
* @return 0 or 1 (false or true)
*
*/
int gpg_pin_is_verified(int pinID) {
int idx;
idx = gpg_pin_get_state_index(pinID);
if (idx >= 0) {
return G_gpg_vstate.verified_pin[idx];
}
return 0;
int idx;
idx = gpg_pin_get_state_index(pinID);
if (idx >= 0) {
return G_gpg_vstate.verified_pin[idx];
}
return 0;
}
/**
* Check if the selected Pin is blocked
*
* @param[in] pin PinCode reference to check
*
* @return 0 or 1 (false or true)
*
*/
int gpg_pin_is_blocked(gpg_pin_t *pin) {
return pin->counter == 0;
return pin->counter == 0;
}
/**
* APDU handler to Verify PinCode
*
* @return Status Word
*
*/
int gpg_apdu_verify() {
gpg_pin_t *pin;
int sw = SW_UNKNOWN;
gpg_pin_t *pin;
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
if (pin == NULL) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
// PINPAD
if (G_gpg_vstate.io_cla == 0xEF) {
if (gpg_pin_is_blocked(pin)) {
THROW(SW_PIN_BLOCKED);
return SW_PIN_BLOCKED;
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
if (pin == NULL) {
return SW_WRONG_DATA;
}
if (G_gpg_vstate.pinmode == PIN_MODE_SCREEN) {
// Delegate pin check to ui
gpg_io_discard(1);
ui_menu_pinentry_display(0);
return 0;
}
if (G_gpg_vstate.pinmode == PIN_MODE_CONFIRM) {
// Delegate pin check to ui
gpg_io_discard(1);
ui_menu_pinconfirm_display(0);
return 0;
// PINPAD
if (G_gpg_vstate.io_cla == CLA_APP_APDU_PIN) {
if (gpg_pin_is_blocked(pin)) {
return SW_PIN_BLOCKED;
}
switch (G_gpg_vstate.pinmode) {
case PIN_MODE_SCREEN:
// Delegate pin check to ui
gpg_io_discard(1);
ui_menu_pinentry_display(0);
sw = 0;
break;
case PIN_MODE_CONFIRM:
// Delegate pin check to ui
gpg_io_discard(1);
ui_menu_pinconfirm_display(G_gpg_vstate.io_p2);
sw = 0;
break;
case PIN_MODE_TRUST:
gpg_pin_set_verified(G_gpg_vstate.io_p2, 1);
gpg_io_discard(1);
sw = 0;
break;
default:
sw = SW_WRONG_DATA;
break;
}
return sw;
}
if (G_gpg_vstate.pinmode == PIN_MODE_TRUST) {
gpg_pin_set_verified(G_gpg_vstate.io_p2, 1);
gpg_io_discard(1);
return SW_OK;
}
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
// NORMAL CHECK
if ((G_gpg_vstate.io_p1 == 0) && G_gpg_vstate.io_length) {
if (gpg_pin_is_blocked(pin)) {
THROW(SW_PIN_BLOCKED);
return SW_PIN_BLOCKED;
// NORMAL CHECK
if ((G_gpg_vstate.io_p1 == PIN_VERIFY) && G_gpg_vstate.io_length) {
if (gpg_pin_is_blocked(pin)) {
return SW_PIN_BLOCKED;
}
sw = gpg_pin_check(pin,
G_gpg_vstate.io_p2,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
G_gpg_vstate.io_length);
gpg_io_discard(1);
return sw;
}
gpg_pin_check_throw(pin, G_gpg_vstate.io_p2, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
G_gpg_vstate.io_length);
gpg_io_discard(1);
return SW_OK;
}
gpg_io_discard(1);
gpg_io_discard(1);
// STATUS REQUEST
if ((G_gpg_vstate.io_p1 == 0) && G_gpg_vstate.io_length == 0) {
if (gpg_pin_is_verified(G_gpg_vstate.io_p2)) {
return SW_OK;
// STATUS REQUEST
if ((G_gpg_vstate.io_p1 == PIN_VERIFY) && G_gpg_vstate.io_length == 0) {
if (gpg_pin_is_verified(G_gpg_vstate.io_p2)) {
return SW_OK;
}
return SW_PWD_NOT_CHECKED | pin->counter;
}
return 0x63C0 | pin->counter;
}
// RESET REQUEST
if ((G_gpg_vstate.io_p1 == 0xFF) && G_gpg_vstate.io_length == 0) {
gpg_pin_set_verified(G_gpg_vstate.io_p2, 0);
return SW_OK;
}
// RESET REQUEST
if ((G_gpg_vstate.io_p1 == PIN_NOT_VERIFIED) && G_gpg_vstate.io_length == 0) {
gpg_pin_set_verified(G_gpg_vstate.io_p2, 0);
return SW_OK;
}
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
return SW_WRONG_DATA;
}
/**
* APDU handler to Change PinCode
*
* @return Status Word
*
*/
int gpg_apdu_change_ref_data() {
gpg_pin_t *pin;
int len, newlen;
int sw = SW_UNKNOWN;
gpg_pin_t *pin;
int len, newlen;
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
if (pin == NULL) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
gpg_pin_set_verified(pin->ref, 0);
// --- PW1/PW3 pin ---
if (gpg_pin_is_blocked(pin)) {
THROW(SW_PIN_BLOCKED);
return SW_PIN_BLOCKED;
}
// avoid any-overflow whitout giving info
if (G_gpg_vstate.io_length == 0) {
if (G_gpg_vstate.pinmode != PIN_MODE_HOST) {
// Delegate pin change to ui
gpg_io_discard(1);
ui_menu_pinentry_display(0);
return 0;
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
if (pin == NULL) {
return SW_WRONG_DATA;
}
}
if (pin->length > G_gpg_vstate.io_length) {
len = G_gpg_vstate.io_length;
} else {
len = pin->length;
}
gpg_pin_set_verified(pin->ref, 0);
gpg_pin_check_throw(pin, pin->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
// --- PW1/PW3 pin ---
if (gpg_pin_is_blocked(pin)) {
return SW_PIN_BLOCKED;
}
// avoid any-overflow without giving info
if (G_gpg_vstate.io_length == 0) {
// Delegate pin change to ui
gpg_io_discard(1);
ui_menu_pinentry_display(0);
return 0;
}
newlen = G_gpg_vstate.io_length - len;
if ((newlen > GPG_MAX_PW_LENGTH) || ((pin->ref == PIN_ID_PW1) && (newlen < 6)) ||
((pin->ref == PIN_ID_PW3) && (newlen < 8))) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
gpg_pin_set(pin, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + len, newlen);
gpg_io_discard(1);
return SW_OK;
len = MIN(G_gpg_vstate.io_length, pin->length);
sw = gpg_pin_check(pin, pin->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
if (sw != SW_OK) {
return sw;
}
newlen = G_gpg_vstate.io_length - len;
if ((newlen > GPG_MAX_PW_LENGTH) ||
((pin->ref == PIN_ID_PW1) && (newlen < GPG_MIN_PW1_LENGTH)) ||
((pin->ref == PIN_ID_PW3) && (newlen < GPG_MIN_PW3_LENGTH))) {
return SW_WRONG_DATA;
}
sw = gpg_pin_set(pin, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + len, newlen);
gpg_io_discard(1);
return sw;
}
/**
* APDU handler to Reste PinCode or Counter
*
* @return Status Word
*
*/
int gpg_apdu_reset_retry_counter() {
gpg_pin_t *pin_pw1;
gpg_pin_t *pin_pw3;
gpg_pin_t *pin_rc;
int rc_len, pw1_len;
pin_pw1 = gpg_pin_get_pin(PIN_ID_PW1);
pin_pw3 = gpg_pin_get_pin(PIN_ID_PW3);
pin_rc = gpg_pin_get_pin(PIN_ID_RC);
if (G_gpg_vstate.io_p1 == 2) {
if (!gpg_pin_is_verified(PIN_ID_PW3)) {
THROW(SW_SECURITY_STATUS_NOT_SATISFIED);
return SW_SECURITY_STATUS_NOT_SATISFIED;
}
rc_len = 0;
pw1_len = G_gpg_vstate.io_length;
} else {
// avoid any-overflow whitout giving info
if (pin_rc->length > G_gpg_vstate.io_length) {
rc_len = G_gpg_vstate.io_length;
int sw = SW_UNKNOWN;
gpg_pin_t *pin_pw1;
gpg_pin_t *pin_rc;
int rc_len, pw1_len;
pin_pw1 = gpg_pin_get_pin(PIN_ID_PW1);
pin_rc = gpg_pin_get_pin(PIN_ID_RC);
if (G_gpg_vstate.io_p1 == RESET_RETRY_WITH_PW3) {
// PW3 must be verified, and the data contain the new PW1
if (!gpg_pin_is_verified(PIN_ID_PW3)) {
return SW_SECURITY_STATUS_NOT_SATISFIED;
}
rc_len = 0;
pw1_len = G_gpg_vstate.io_length;
} else {
rc_len = pin_rc->length;
// The data contain the Resetting Code + the new PW1
// avoid any-overflow without giving info
rc_len = MIN(G_gpg_vstate.io_length, pin_rc->length);
pw1_len = G_gpg_vstate.io_length - rc_len;
sw = gpg_pin_check(pin_rc,
pin_rc->ref,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
rc_len);
if (sw != SW_OK) {
return sw;
}
}
pw1_len = G_gpg_vstate.io_length - rc_len;
gpg_pin_check_throw(pin_rc, pin_rc->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, rc_len);
}
if ((pw1_len > GPG_MAX_PW_LENGTH) || (pw1_len < 6)) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
gpg_pin_set(pin_pw1, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + rc_len, pw1_len);
gpg_io_discard(1);
return SW_OK;
if ((pw1_len > GPG_MAX_PW_LENGTH) || (pw1_len < GPG_MIN_PW1_LENGTH)) {
return SW_WRONG_DATA;
}
sw = gpg_pin_set(pin_pw1,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + rc_len,
pw1_len);
gpg_io_discard(1);
return sw;
}

@ -1,338 +1,433 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "gpg_ux_nanos.h"
#include "gpg_ux.h"
#include "cx_errors.h"
const unsigned char gpg_oid_sha256[] = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
const unsigned char gpg_oid_sha512[] = {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40};
const unsigned char gpg_oid_sha256[] = {0x30,
0x31,
0x30,
0x0d,
0x06,
0x09,
0x60,
0x86,
0x48,
0x01,
0x65,
0x03,
0x04,
0x02,
0x01,
0x05,
0x00,
0x04,
0x20};
const unsigned char gpg_oid_sha512[] = {0x30,
0x51,
0x30,
0x0d,
0x06,
0x09,
0x60,
0x86,
0x48,
0x01,
0x65,
0x03,
0x04,
0x02,
0x03,
0x05,
0x00,
0x04,
0x40};
/**
* Reset PW1 verified status
*
*/
static void gpg_pso_reset_PW1() {
if (N_gpg_pstate->PW_status[0] == 0) {
gpg_pin_set_verified(PIN_ID_PW1, 0);
}
if (N_gpg_pstate->PW_status[0] == 0) {
gpg_pin_set_verified(PIN_ID_PW1, 0);
}
}
/**
* Perform a Digital Signature
*
* @param[in] sigKey signing key
*
* @return Status Word
*
*/
static int gpg_sign(gpg_key_t *sigkey) {
// --- RSA
if (sigkey->attributes.value[0] == 1) {
cx_rsa_private_key_t *key;
unsigned int ksz, l;
ksz = (sigkey->attributes.value[1] << 8) | sigkey->attributes.value[2];
ksz = ksz >> 3;
switch (ksz) {
case 1024 / 8:
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa1024;
break;
case 2048 / 8:
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa2048;
break;
case 3072 / 8:
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa3072;
break;
case 4096 / 8:
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa4096;
break;
}
if (key->size != ksz) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
cx_err_t error = CX_INTERNAL_ERROR;
cx_rsa_private_key_t *rsa_key = NULL;
unsigned int ksz, l;
cx_ecfp_private_key_t *ecfp_key = NULL;
unsigned int s_len, i, rs_len, info;
unsigned char *rs;
// sign
if (ksz < G_gpg_vstate.io_length) {
THROW(SW_WRONG_LENGTH);
}
l = ksz - G_gpg_vstate.io_length;
os_memmove(G_gpg_vstate.work.io_buffer + l, G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_length);
os_memset(G_gpg_vstate.work.io_buffer, 0xFF, l);
G_gpg_vstate.work.io_buffer[0] = 0;
G_gpg_vstate.work.io_buffer[1] = 1;
G_gpg_vstate.work.io_buffer[l - 1] = 0;
ksz = cx_rsa_decrypt(key, CX_PAD_NONE, CX_NONE, G_gpg_vstate.work.io_buffer, ksz, G_gpg_vstate.work.io_buffer, ksz);
// send
gpg_io_discard(0);
gpg_io_inserted(ksz);
gpg_pso_reset_PW1();
return SW_OK;
}
// --- ECDSA/EdDSA
if ((sigkey->attributes.value[0] == 19) || (sigkey->attributes.value[0] == 22)) {
cx_ecfp_private_key_t *key;
unsigned int sz, i, rs_len, info;
unsigned char * rs;
switch (sigkey->attributes.value[0]) {
case KEY_ID_RSA:
ksz = U2BE(sigkey->attributes.value, 1) >> 3;
switch (ksz) {
case 2048 / 8:
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa2048;
break;
case 3072 / 8:
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa3072;
break;
#ifdef WITH_SUPPORT_RSA4096
case 4096 / 8:
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa4096;
break;
#endif
default:
break;
}
if ((rsa_key == NULL) || (rsa_key->size != ksz)) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
key = &sigkey->priv_key.ecfp;
// sign
if (ksz < G_gpg_vstate.io_length) {
error = SW_WRONG_LENGTH;
break;
}
l = ksz - G_gpg_vstate.io_length;
memmove(G_gpg_vstate.work.io_buffer + l,
G_gpg_vstate.work.io_buffer,
G_gpg_vstate.io_length);
memset(G_gpg_vstate.work.io_buffer, 0xFF, l);
G_gpg_vstate.work.io_buffer[0] = 0;
G_gpg_vstate.work.io_buffer[1] = 1;
G_gpg_vstate.work.io_buffer[l - 1] = 0;
CX_CHECK(cx_rsa_decrypt_no_throw(rsa_key,
CX_PAD_NONE,
CX_NONE,
G_gpg_vstate.work.io_buffer,
ksz,
G_gpg_vstate.work.io_buffer,
&ksz));
// send
gpg_io_discard(0);
gpg_io_inserted(ksz);
gpg_pso_reset_PW1();
error = SW_OK;
break;
// sign
#define RS (G_gpg_vstate.work.io_buffer + (GPG_IO_BUFFER_LENGTH - 256))
if (sigkey->attributes.value[0] == 19) {
sz = gpg_curve2domainlen(key->curve);
if ((sz == 0) || (key->d_len != sz)) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
sz = cx_ecdsa_sign(key, CX_RND_TRNG, CX_NONE, G_gpg_vstate.work.io_buffer, sz, RS, 256, &info);
// reencode r,s in MPI format
gpg_io_discard(0);
case KEY_ID_ECDSA:
ecfp_key = &sigkey->priv_key.ecfp;
ksz = (unsigned int) gpg_curve2domainlen(ecfp_key->curve);
if ((ksz == 0) || (ecfp_key->d_len != ksz)) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
s_len = 256;
CX_CHECK(cx_ecdsa_sign_no_throw(ecfp_key,
CX_RND_TRNG,
CX_NONE,
G_gpg_vstate.work.io_buffer,
ksz,
RS,
&s_len,
&info));
// re-encode r,s in MPI format
gpg_io_discard(0);
rs_len = RS[3];
rs = &RS[4];
rs_len = RS[3];
rs = &RS[4];
for (i = 0; i < 2; i++) {
if (*rs == 0) {
rs++;
rs_len--;
}
gpg_io_insert_u8(0);
gpg_io_insert(rs, rs_len);
rs = rs + rs_len;
rs_len = rs[1];
rs += 2;
}
} else {
sz = cx_eddsa_sign(key, CX_NONE, CX_SHA512, G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_length, NULL, 0, RS, 256,
NULL);
gpg_io_discard(0);
gpg_io_insert(RS, sz);
for (i = 0; i < 2; i++) {
if (*rs == 0) {
rs++;
rs_len--;
}
gpg_io_insert_u8(0);
gpg_io_insert(rs, rs_len);
rs = rs + rs_len;
rs_len = rs[1];
rs += 2;
}
error = SW_OK;
break;
case KEY_ID_EDDSA:
ecfp_key = &sigkey->priv_key.ecfp;
ksz = 256;
CX_CHECK(cx_eddsa_sign_no_throw(ecfp_key,
CX_SHA512,
G_gpg_vstate.work.io_buffer,
G_gpg_vstate.io_length,
RS,
ksz));
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
ksz *= 2;
gpg_io_discard(0);
gpg_io_insert(RS, ksz);
error = SW_OK;
break;
default:
// --- PSO:CDS NOT SUPPORTED
error = SW_REFERENCED_DATA_NOT_FOUND;
break;
}
#undef RS
// send
end:
gpg_pso_reset_PW1();
return SW_OK;
}
// --- PSO:CDS NOT SUPPORTED
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
return error;
}
/**
* APDU handler to Perform Security Operation
*
* @return Status Word
*
*/
int gpg_apdu_pso() {
unsigned int t, l, ksz;
cx_err_t error = CX_INTERNAL_ERROR;
unsigned int t, l, ksz;
unsigned int cnt, pad_byte;
unsigned int msg_len;
unsigned int curve;
cx_aes_key_t *aes_key = NULL;
cx_rsa_private_key_t *rsa_key = NULL;
cx_ecfp_private_key_t *ecfp_key = NULL;
unsigned int pso;
// UIF HANDLE
switch (G_gpg_vstate.io_p1p2) {
// --- PSO:CDS ---
case PSO_CDS:
if (G_gpg_vstate.kslot->sig.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
}
G_gpg_vstate.UIF_flags = 0;
}
break;
// --- PSO:DEC ---
case PSO_DEC:
case PSO_ENC:
if (G_gpg_vstate.kslot->dec.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
}
G_gpg_vstate.UIF_flags = 0;
}
break;
}
pso = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
// --- PSO:ENC ---
switch (G_gpg_vstate.io_p1p2) {
case PSO_CDS:
error = gpg_sign(&G_gpg_vstate.kslot->sig);
cnt = G_gpg_vstate.kslot->sig_count + 1;
nvm_write(&G_gpg_vstate.kslot->sig_count, &cnt, sizeof(unsigned int));
break;
// UIF HANDLE
switch (pso) {
// --- PSO:CDS ---
case 0x9e9a:
if (G_gpg_vstate.kslot->sig.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
}
G_gpg_vstate.UIF_flags = 0;
}
break;
// --- PSO:DEC ---
case 0x8086:
case 0x8680:
if (G_gpg_vstate.kslot->dec.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
}
G_gpg_vstate.UIF_flags = 0;
}
break;
}
case PSO_ENC:
aes_key = &G_gpg_vstate.kslot->AES_dec;
if (!(aes_key->size != CX_AES_128_KEY_LEN)) {
return SW_CONDITIONS_NOT_SATISFIED;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
ksz = GPG_IO_BUFFER_LENGTH - 1;
CX_CHECK(cx_aes_no_throw(aes_key,
CX_ENCRYPT | CX_CHAIN_CBC | CX_LAST,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
msg_len,
G_gpg_vstate.work.io_buffer + 1,
&ksz));
// send
gpg_io_discard(0);
G_gpg_vstate.work.io_buffer[0] = PAD_AES;
gpg_io_inserted(1 + ksz);
error = SW_OK;
break;
// --- PSO:ENC ---
switch (pso) {
// --- PSO:CDS ---
case 0x9e9a: {
unsigned int cnt;
int sw;
sw = gpg_sign(&G_gpg_vstate.kslot->sig);
cnt = G_gpg_vstate.kslot->sig_count + 1;
nvm_write(&G_gpg_vstate.kslot->sig_count, &cnt, sizeof(unsigned int));
return sw;
}
// --- PSO:ENC ---
case 0x8680: {
unsigned int msg_len;
cx_aes_key_t *key;
unsigned int sz;
key = &G_gpg_vstate.kslot->AES_dec;
if (!(key->size != 16)) {
THROW(SW_CONDITIONS_NOT_SATISFIED + 5);
return SW_CONDITIONS_NOT_SATISFIED;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
sz = cx_aes(key, CX_ENCRYPT | CX_CHAIN_CBC | CX_LAST, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, msg_len,
G_gpg_vstate.work.io_buffer + 1, GPG_IO_BUFFER_LENGTH - 1);
// send
gpg_io_discard(0);
G_gpg_vstate.work.io_buffer[0] = 0x02;
gpg_io_inserted(1 + sz);
return SW_OK;
}
case PSO_DEC:
pad_byte = gpg_io_fetch_u8();
// --- PSO:DEC ---
case 0x8086: {
unsigned int msg_len;
unsigned int pad_byte;
unsigned int sz;
pad_byte = gpg_io_fetch_u8();
switch (pad_byte) {
case PAD_RSA:
if (G_gpg_vstate.mse_dec->attributes.value[0] != KEY_ID_RSA) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
ksz = U2BE(G_gpg_vstate.mse_dec->attributes.value, 1) >> 3;
switch (ksz) {
case 2048 / 8:
rsa_key =
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa2048;
break;
case 3072 / 8:
rsa_key =
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa3072;
break;
#ifdef WITH_SUPPORT_RSA4096
case 4096 / 8:
rsa_key =
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa4096;
break;
#endif
}
switch (pad_byte) {
// --- PSO:DEC:RSA
case 0x00: {
cx_rsa_private_key_t *key;
if (G_gpg_vstate.mse_dec->attributes.value[0] != 0x01) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
ksz = (G_gpg_vstate.mse_dec->attributes.value[1] << 8) | G_gpg_vstate.mse_dec->attributes.value[2];
ksz = ksz >> 3;
key = NULL;
switch (ksz) {
case 1024 / 8:
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa1024;
break;
case 2048 / 8:
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa2048;
break;
case 3072 / 8:
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa3072;
break;
case 4096 / 8:
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa4096;
break;
}
if ((rsa_key == NULL) || (rsa_key->size != ksz)) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
CX_CHECK(cx_rsa_decrypt_no_throw(
rsa_key,
CX_PAD_PKCS1_1o5,
CX_NONE,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
msg_len,
G_gpg_vstate.work.io_buffer,
&ksz));
// send
gpg_io_discard(0);
gpg_io_inserted(ksz);
error = SW_OK;
break;
if ((key == NULL) || (key->size != ksz)) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
sz = cx_rsa_decrypt(key, CX_PAD_PKCS1_1o5, CX_NONE, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, msg_len,
G_gpg_vstate.work.io_buffer, ksz);
// send
gpg_io_discard(0);
gpg_io_inserted(sz);
return SW_OK;
}
case PAD_AES:
aes_key = &G_gpg_vstate.kslot->AES_dec;
if (!(aes_key->size != CX_AES_128_KEY_LEN)) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
ksz = GPG_IO_BUFFER_LENGTH;
CX_CHECK(cx_aes_no_throw(aes_key,
CX_DECRYPT | CX_CHAIN_CBC | CX_LAST,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
msg_len,
G_gpg_vstate.work.io_buffer,
&ksz));
// send
gpg_io_discard(0);
gpg_io_inserted(ksz);
error = SW_OK;
break;
// --- PSO:DEC:AES
case 0x02: {
cx_aes_key_t *key;
unsigned int sz;
key = &G_gpg_vstate.kslot->AES_dec;
if (!(key->size != 16)) {
THROW(SW_CONDITIONS_NOT_SATISFIED + 5);
return SW_CONDITIONS_NOT_SATISFIED;
}
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
sz = cx_aes(key, CX_DECRYPT | CX_CHAIN_CBC | CX_LAST, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
msg_len, G_gpg_vstate.work.io_buffer, GPG_IO_BUFFER_LENGTH);
// send
gpg_io_discard(0);
gpg_io_inserted(sz);
return SW_OK;
}
case PAD_ECDH:
if (G_gpg_vstate.mse_dec->attributes.value[0] != KEY_ID_ECDH) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
ecfp_key = &G_gpg_vstate.mse_dec->priv_key.ecfp;
gpg_io_fetch_l(&l);
gpg_io_fetch_tl(&t, &l);
if (t != 0x7f49) {
error = SW_WRONG_DATA;
break;
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x86) {
error = SW_WRONG_DATA;
break;
}
// --- PSO:DEC:ECDH
case 0xA6: {
cx_ecfp_private_key_t *key;
unsigned int sz;
unsigned int curve;
if (G_gpg_vstate.mse_dec->attributes.value[0] != 0x12) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
key = &G_gpg_vstate.mse_dec->priv_key.ecfp;
gpg_io_fetch_l(&l);
gpg_io_fetch_tl(&t, &l);
if (t != 0x7f49) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
gpg_io_fetch_tl(&t, &l);
if (t != 0x86) {
THROW(SW_WRONG_DATA);
return SW_WRONG_DATA;
}
curve = gpg_oid2curve(G_gpg_vstate.mse_dec->attributes.value + 1,
G_gpg_vstate.mse_dec->attributes.length - 1);
if (ecfp_key->curve != curve) {
error = SW_CONDITIONS_NOT_SATISFIED;
break;
}
if (curve == CX_CURVE_Curve25519) {
for (cnt = 0; cnt <= 31; cnt++) {
G_gpg_vstate.work.io_buffer[512 + cnt] =
(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset)[31 - cnt];
}
G_gpg_vstate.work.io_buffer[511] = 0x02;
CX_CHECK(cx_ecdh_no_throw(ecfp_key,
CX_ECDH_X,
G_gpg_vstate.work.io_buffer + 511,
65,
G_gpg_vstate.work.io_buffer + 256,
160));
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
curve = gpg_oid2curve(G_gpg_vstate.mse_dec->attributes.value + 1, G_gpg_vstate.mse_dec->attributes.length - 1);
if (key->curve != curve) {
THROW(SW_CONDITIONS_NOT_SATISFIED);
return SW_CONDITIONS_NOT_SATISFIED;
}
if (curve == CX_CURVE_Curve25519) {
unsigned int i;
for (cnt = 0; cnt <= 31; cnt++) {
G_gpg_vstate.work.io_buffer[128 + cnt] =
G_gpg_vstate.work.io_buffer[287 - cnt];
}
ksz = 32;
} else {
CX_CHECK(
cx_ecdh_no_throw(ecfp_key,
CX_ECDH_X,
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
65,
G_gpg_vstate.work.io_buffer + 128,
160));
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
}
// send
gpg_io_discard(0);
gpg_io_insert(G_gpg_vstate.work.io_buffer + 128, ksz);
error = SW_OK;
break;
for (i = 0; i <= 31; i++) {
G_gpg_vstate.work.io_buffer[512 + i] = (G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset)[31 - i];
}
G_gpg_vstate.work.io_buffer[511] = 0x02;
sz = cx_ecdh(key, CX_ECDH_X, G_gpg_vstate.work.io_buffer + 511, 65, G_gpg_vstate.work.io_buffer + 256, 160);
for (i = 0; i <= 31; i++) {
G_gpg_vstate.work.io_buffer[128 + i] = G_gpg_vstate.work.io_buffer[287 - i];
}
sz = 32;
} else {
sz = cx_ecdh(key, CX_ECDH_X, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, 65,
G_gpg_vstate.work.io_buffer + 128, 160);
}
// send
gpg_io_discard(0);
gpg_io_insert(G_gpg_vstate.work.io_buffer + 128, sz);
return SW_OK;
}
// --- PSO:DEC:xx NOT SUPPORTED
default:
error = SW_REFERENCED_DATA_NOT_FOUND;
break;
}
break;
// --- PSO:DEC:xx NOT SUPPORTDED
default:
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
//--- PSO:yy NOT SUPPORTED ---
default:
error = SW_REFERENCED_DATA_NOT_FOUND;
break;
}
}
//--- PSO:yy NOT SUPPPORTED ---
default:
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
}
THROW(SW_REFERENCED_DATA_NOT_FOUND);
return SW_REFERENCED_DATA_NOT_FOUND;
end:
return error;
}
/**
* APDU handler to Internal Authentication
*
* @return Status Word
*
*/
int gpg_apdu_internal_authenticate() {
// --- PSO:AUTH ---
if (G_gpg_vstate.kslot->aut.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
// --- PSO:AUTH ---
if (G_gpg_vstate.kslot->aut.UIF[0]) {
if ((G_gpg_vstate.UIF_flags) == 0) {
ui_menu_uifconfirm_display(0);
return 0;
}
G_gpg_vstate.UIF_flags = 0;
}
G_gpg_vstate.UIF_flags = 0;
}
if (G_gpg_vstate.mse_aut->attributes.value[0] == 1) {
if (G_gpg_vstate.io_length >
((G_gpg_vstate.mse_aut->attributes.value[1] << 8) | G_gpg_vstate.mse_aut->attributes.value[2]) * 40 / 100) {
THROW(SW_WRONG_LENGTH);
return SW_WRONG_LENGTH;
if (G_gpg_vstate.mse_aut->attributes.value[0] == KEY_ID_RSA) {
if (G_gpg_vstate.io_length > U2BE(G_gpg_vstate.mse_aut->attributes.value, 1) * 40 / 100) {
return SW_WRONG_LENGTH;
}
}
}
return gpg_sign(G_gpg_vstate.mse_aut);
return gpg_sign(G_gpg_vstate.mse_aut);
}

@ -1,42 +0,0 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "os_io_seproxyhal.h"
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
#include "ux.h"
ux_state_t G_ux;
bolos_ux_params_t G_ux_params;
#else
ux_state_t ux;
#endif
#ifndef GPG_DEBUG_MAIN
unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B];
#else
extern unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B];
int apdu_n;
#endif
gpg_v_state_t G_gpg_vstate;
#ifdef HAVE_RSA
union cx_u G_cx;
#endif // HAVE_RSA

@ -1,57 +1,71 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
const unsigned char C_MF[] = {0x3F, 0x00};
const unsigned char C_ATR[] = {0x2F, 0x02};
/**
* APDU handler to Select the card
*
* @return Status Word
*
*/
int gpg_apdu_select() {
int sw;
int sw = SW_UNKNOWN;
// MF
if ((G_gpg_vstate.io_length == 2) && (os_memcmp(G_gpg_vstate.work.io_buffer, C_MF, G_gpg_vstate.io_length) == 0)) {
gpg_io_discard(0);
sw = SW_OK;
}
// AID APP
else if ((G_gpg_vstate.io_length == 6) &&
(os_memcmp(G_gpg_vstate.work.io_buffer, N_gpg_pstate->AID, G_gpg_vstate.io_length) == 0)) {
G_gpg_vstate.DO_current = 0;
G_gpg_vstate.DO_reccord = 0;
G_gpg_vstate.DO_offset = 0;
if (G_gpg_vstate.selected == 0) {
G_gpg_vstate.verified_pin[0] = 0;
G_gpg_vstate.verified_pin[1] = 0;
G_gpg_vstate.verified_pin[2] = 0;
G_gpg_vstate.verified_pin[3] = 0;
G_gpg_vstate.verified_pin[4] = 0;
// MF
if ((G_gpg_vstate.io_length == sizeof(C_MF)) &&
(memcmp(G_gpg_vstate.work.io_buffer, C_MF, G_gpg_vstate.io_length) == 0)) {
gpg_io_discard(0);
sw = SW_OK;
}
// EF.ATR
else if ((G_gpg_vstate.io_length == sizeof(C_ATR)) &&
(memcmp(G_gpg_vstate.work.io_buffer, C_ATR, G_gpg_vstate.io_length) == 0)) {
gpg_io_discard(0);
sw = SW_OK;
}
// AID APP
else if ((G_gpg_vstate.io_length == 6) && (memcmp(G_gpg_vstate.work.io_buffer,
(const void *) N_gpg_pstate->AID,
G_gpg_vstate.io_length) == 0)) {
G_gpg_vstate.DO_current = 0;
G_gpg_vstate.DO_reccord = 0;
G_gpg_vstate.DO_offset = 0;
if (G_gpg_vstate.selected == 0) {
G_gpg_vstate.verified_pin[0] = 0;
G_gpg_vstate.verified_pin[1] = 0;
G_gpg_vstate.verified_pin[2] = 0;
G_gpg_vstate.verified_pin[3] = 0;
G_gpg_vstate.verified_pin[4] = 0;
}
gpg_io_discard(0);
if (N_gpg_pstate->histo[7] != 0x07) {
THROW(SW_STATE_TERMINATED);
gpg_io_discard(0);
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] != STATE_ACTIVATE) {
sw = SW_STATE_TERMINATED;
} else {
sw = SW_OK;
}
}
// NOT FOUND
else {
sw = SW_FILE_NOT_FOUND;
}
sw = SW_OK;
}
// NOT FOUND
else {
THROW(SW_FILE_NOT_FOUND);
return SW_FILE_NOT_FOUND;
}
return sw;
return sw;
}

@ -1,23 +1,29 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_TYPES_H
#define GPG_TYPES_H
#include "os_io_seproxyhal.h"
#include "ux.h"
#include "lcx_sha3.h"
#include "usbd_ccid_if.h"
#include "bolos_target.h"
#ifdef HAVE_NBGL
#include "nbgl_layout.h"
#endif
/* cannot send more that F0 bytes in CCID, why? do not know for now
* So set up length to F0 minus 2 bytes for SW
@ -26,155 +32,160 @@
/* big private DO */
#define GPG_EXT_PRIVATE_DO_LENGTH 512
/* will be fixed..1024 is not enougth */
/* will be fixed..1024 is not enough */
#define GPG_EXT_CARD_HOLDER_CERT_LENTH 2560
/* random choice */
#define GPG_EXT_CHALLENGE_LENTH 254
/* accpet long PW, but less than one sha256 block */
#define GPG_MAX_PW_LENGTH 12
#if GPG_MULTISLOT
#define GPG_KEYS_SLOTS 3
#else
/* accept long PW, but less than one sha256 block */
#define GPG_MAX_PW_LENGTH 12
#define GPG_MIN_PW1_LENGTH 6
#define GPG_MIN_PW3_LENGTH 8
#define MAGIC_LENGTH 8
#define AID_LENGTH 16
#define HISTO_LENGTH 15
#define HISTO_OFFSET_STATE 12 // 3rd byte from last (buffer size is 15)
#ifdef TARGET_NANOS
#define GPG_KEYS_SLOTS 1
#else
#define GPG_KEYS_SLOTS 3
#endif
#define GPG_KEY_ATTRIBUTES_LENGTH 12
#define GPG_RSA_DEFAULT_PUB 0x00010001U
#ifndef CX_AES_128_KEY_LEN
#define CX_AES_128_KEY_LEN CX_AES_BLOCK_SIZE
#endif
/* --- Keys IDs --- */
// https://www.rfc-editor.org/rfc/rfc4880#section-9.1
#define KEY_ID_RSA 1 // RSA (Encrypt or Sign)
#define KEY_ID_ECDH 18 // Elliptic Curve Diffie-Hellman
#define KEY_ID_ECDSA 19 // Elliptic Curve Digital Signature Algorithm
#define KEY_ID_EDDSA 22 // Edwards-curve Digital Signature Algorithm
struct gpg_pin_s {
unsigned int ref;
// initial pin length, 0 means not set
unsigned int length;
unsigned int counter;
// only store sha256 of PIN/RC
unsigned char value[32];
unsigned int ref;
// initial pin length, 0 means not set
unsigned int length;
unsigned int counter;
// only store sha256 of PIN/RC
unsigned char value[32];
};
typedef struct gpg_pin_s gpg_pin_t;
#define LV(name, maxlen) \
struct { \
unsigned int length; \
unsigned char value[maxlen]; \
} name
typedef struct gpg_lv_s {
unsigned int length;
unsigned char value[];
} gpg_lv_t;
#define LV(name, maxlen) \
struct { \
unsigned int length; \
unsigned char value[maxlen]; \
} name
typedef struct gpg_key_s {
/* C1 C2 C3 */
LV(attributes, GPG_KEY_ATTRIBUTES_LENGTH);
/* key value */
/* WARN: changing the cx_<key>_t structures breaks backup/restore. Adapt backup/restore code
* to ensure backward compatibility.
*/
union {
cx_rsa_private_key_t rsa;
cx_rsa_1024_private_key_t rsa1024;
cx_rsa_2048_private_key_t rsa2048;
cx_rsa_3072_private_key_t rsa3072;
cx_rsa_4096_private_key_t rsa4096;
cx_ecfp_private_key_t ecfp;
cx_ecfp_256_private_key_t ecfp256;
cx_ecfp_384_private_key_t ecfp384;
cx_ecfp_512_private_key_t ecfp512;
cx_ecfp_640_private_key_t ecfp640;
} priv_key;
union {
unsigned char rsa[4];
cx_ecfp_public_key_t ecfp;
cx_ecfp_256_public_key_t ecfp256;
cx_ecfp_384_public_key_t ecfp384;
cx_ecfp_512_public_key_t ecfp512;
cx_ecfp_640_public_key_t ecfp640;
} pub_key;
/* C7 C8 C9 , C5 = C7|C8|C9*/
unsigned char fingerprints[20];
/* 7F21 */
LV(CA, GPG_EXT_CARD_HOLDER_CERT_LENTH);
/* C7 C8 C9, C6 = C7|C8|C9*/
unsigned char CA_fingerprints[20];
/* CE CF D0, CD = CE|CF|D0 */
unsigned char date[4];
/* D6/D7/D8- */
unsigned char UIF[2];
/* C1 C2 C3 */
LV(attributes, GPG_KEY_ATTRIBUTES_LENGTH);
/* key value */
union {
cx_rsa_private_key_t rsa;
cx_rsa_2048_private_key_t rsa2048;
cx_rsa_3072_private_key_t rsa3072;
#ifdef WITH_SUPPORT_RSA4096
cx_rsa_4096_private_key_t rsa4096;
#endif
cx_ecfp_private_key_t ecfp;
cx_ecfp_256_private_key_t ecfp256;
cx_ecfp_384_private_key_t ecfp384;
cx_ecfp_512_private_key_t ecfp512;
cx_ecfp_640_private_key_t ecfp640;
} priv_key;
union {
unsigned char rsa[4];
cx_ecfp_public_key_t ecfp;
cx_ecfp_256_public_key_t ecfp256;
cx_ecfp_384_public_key_t ecfp384;
cx_ecfp_512_public_key_t ecfp512;
cx_ecfp_640_public_key_t ecfp640;
} pub_key;
/* C7 C8 C9 , C5 = C7|C8|C9*/
unsigned char fingerprints[20];
/* 7F21 */
LV(CA, GPG_EXT_CARD_HOLDER_CERT_LENTH);
/* C7 C8 C9, C6 = C7|C8|C9*/
unsigned char CA_fingerprints[20];
/* CE CF D0, CD = CE|CF|D0 */
unsigned char date[4];
/* D6/D7/D8- */
unsigned char UIF[2];
} gpg_key_t;
typedef struct gpg_key_slot_s {
unsigned char serial[4];
/* */
gpg_key_t sig;
gpg_key_t aut;
gpg_key_t dec;
/* -- Security support template -- */
/* 93 */
unsigned int sig_count;
/* D5 */
cx_aes_key_t AES_dec;
unsigned char serial[4];
/* */
gpg_key_t sig;
gpg_key_t aut;
gpg_key_t dec;
/* -- Security support template -- */
/* 93 */
unsigned int sig_count;
/* D5 */
cx_aes_key_t AES_dec;
/* 5F50 */
LV(url, GPG_EXT_PRIVATE_DO_LENGTH);
} gpg_key_slot_t;
struct gpg_nv_state_s {
/* magic */
unsigned char magic[8];
/* pin mode */
unsigned char config_pin[1];
/* 01F1 (01F2 is volatile)*/
unsigned char config_slot[3];
/* RSA exponent */
unsigned char default_RSA_exponent[4];
/* 0101 0102 0103 0104 */
LV(private_DO1, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO2, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO3, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO4, GPG_EXT_PRIVATE_DO_LENGTH);
/* 5E */
LV(login, GPG_EXT_PRIVATE_DO_LENGTH);
/* 5F50 */
LV(url, GPG_EXT_PRIVATE_DO_LENGTH);
/* -- Cardholder Related Data -- */
/* 5B */
LV(name, 39);
/* 5F2D */
LV(lang, 8);
/* 5F35 */
unsigned char sex[1];
/* -- Application Related Data -- */
/* 4F */
unsigned char AID[16];
/* 5F52 */
unsigned char histo[15];
/* 7f66 */
// unsigned char ext_length_info[8];
/* C0 */
// unsigned char ext_capabilities[10];
/* C4 */
unsigned char PW_status[4];
/* PINs */
gpg_pin_t PW1;
gpg_pin_t PW3;
gpg_pin_t RC;
/* gpg keys */
gpg_key_slot_t keys[GPG_KEYS_SLOTS];
/* --- SM --- */
/* D1 */
cx_aes_key_t SM_enc;
/* D2 */
cx_aes_key_t SM_mac;
/* magic */
unsigned char magic[MAGIC_LENGTH];
/* pin mode */
unsigned char config_pin[1];
/* 01F1 (01F2 is volatile)*/
unsigned char config_slot[3];
/* RSA exponent */
unsigned char default_RSA_exponent[4];
/* 0101 0102 0103 0104 */
LV(private_DO1, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO2, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO3, GPG_EXT_PRIVATE_DO_LENGTH);
LV(private_DO4, GPG_EXT_PRIVATE_DO_LENGTH);
/* 5E */
LV(login, GPG_EXT_PRIVATE_DO_LENGTH);
/* -- Cardholder Related Data -- */
/* 5B */
LV(name, 39);
/* 5F2D */
LV(lang, 8);
/* 5F35 */
unsigned char salutation[1];
/* -- Application Related Data -- */
/* 4F */
unsigned char AID[AID_LENGTH];
/* 5F52 */
unsigned char histo[HISTO_LENGTH];
/* C4 */
unsigned char PW_status[4];
/* PINs */
gpg_pin_t PW1;
gpg_pin_t PW3;
gpg_pin_t RC;
/* gpg keys */
gpg_key_slot_t keys[GPG_KEYS_SLOTS];
/* --- SM --- */
/* D1 */
cx_aes_key_t SM_enc;
/* D2 */
cx_aes_key_t SM_mac;
};
typedef struct gpg_nv_state_s gpg_nv_state_t;
@ -182,215 +193,217 @@ typedef struct gpg_nv_state_s gpg_nv_state_t;
#define GPG_IO_BUFFER_LENGTH (1512)
struct gpg_v_state_s {
/* app state */
unsigned char selected;
unsigned char slot; /* DO 01F2 */
gpg_key_slot_t *kslot;
gpg_key_t * mse_aut;
gpg_key_t * mse_dec;
unsigned char seed_mode;
unsigned char UIF_flags;
/* io state*/
unsigned char io_cla;
unsigned char io_ins;
unsigned char io_p1;
unsigned char io_p2;
unsigned char io_lc;
unsigned char io_le;
unsigned short io_length;
unsigned short io_offset;
unsigned short io_mark;
union {
unsigned char io_buffer[GPG_IO_BUFFER_LENGTH];
struct {
union {
cx_rsa_public_key_t public;
cx_rsa_1024_public_key_t public1024;
cx_rsa_2048_public_key_t public2048;
cx_rsa_3072_public_key_t public3072;
cx_rsa_4096_public_key_t public4096;
};
union {
cx_rsa_private_key_t private;
cx_rsa_1024_private_key_t private1024;
cx_rsa_2048_private_key_t private2048;
cx_rsa_3072_private_key_t private3072;
cx_rsa_4096_private_key_t private4096;
};
} rsa;
struct {
union {
cx_ecfp_public_key_t public;
cx_ecfp_256_public_key_t public256;
cx_ecfp_384_public_key_t public384;
cx_ecfp_512_public_key_t public512;
cx_ecfp_640_public_key_t public640;
};
union {
cx_ecfp_private_key_t private;
cx_ecfp_256_private_key_t private256;
cx_ecfp_384_private_key_t private384;
cx_ecfp_512_private_key_t private512;
cx_ecfp_640_private_key_t private640;
};
} ecfp;
struct {
unsigned char md_buffer[GPG_IO_BUFFER_LENGTH - (32 + MAX(sizeof(cx_sha3_t), sizeof(cx_sha256_t)))];
unsigned char H[32];
union {
cx_sha3_t sha3;
cx_sha256_t sha256;
};
} md;
} work;
/* data state */
unsigned short DO_current;
unsigned short DO_reccord;
unsigned short DO_offset;
/* PINs state */
unsigned char verified_pin[5];
unsigned char pinmode;
/* ux menus */
char menu[112];
unsigned char ux_pinentry[12];
unsigned int ux_key;
unsigned int ux_type;
#ifdef UI_NANO_S
ux_menu_entry_t ui_dogsays[2];
/* app state */
unsigned char selected;
unsigned char slot; /* DO 01F2 */
gpg_key_slot_t *kslot;
gpg_key_t *mse_aut;
gpg_key_t *mse_dec;
unsigned char seed_mode;
unsigned char UIF_flags;
/* io state*/
unsigned char io_cla;
unsigned char io_ins;
unsigned char io_p1;
unsigned char io_p2;
unsigned char io_lc;
unsigned char io_le;
unsigned short io_length;
unsigned short io_offset;
unsigned short io_mark;
unsigned short io_p1p2;
union {
unsigned char io_buffer[GPG_IO_BUFFER_LENGTH];
struct {
union {
cx_rsa_public_key_t public;
cx_rsa_2048_public_key_t public2048;
cx_rsa_3072_public_key_t public3072;
#ifdef WITH_SUPPORT_RSA4096
cx_rsa_4096_public_key_t public4096;
#endif
};
union {
cx_rsa_private_key_t private;
cx_rsa_2048_private_key_t private2048;
cx_rsa_3072_private_key_t private3072;
#ifdef WITH_SUPPORT_RSA4096
cx_rsa_4096_private_key_t private4096;
#endif
};
} rsa;
struct {
union {
cx_ecfp_public_key_t public;
cx_ecfp_256_public_key_t public256;
cx_ecfp_384_public_key_t public384;
cx_ecfp_512_public_key_t public512;
cx_ecfp_640_public_key_t public640;
};
union {
cx_ecfp_private_key_t private;
cx_ecfp_256_private_key_t private256;
cx_ecfp_384_private_key_t private384;
cx_ecfp_512_private_key_t private512;
cx_ecfp_640_private_key_t private640;
};
} ecfp;
struct {
unsigned char md_buffer[GPG_IO_BUFFER_LENGTH -
(32 + MAX(sizeof(cx_sha3_t), sizeof(cx_sha256_t)))];
unsigned char H[32];
union {
cx_sha3_t sha3;
cx_sha256_t sha256;
};
} md;
} work;
/* data state */
unsigned short DO_current;
unsigned short DO_reccord;
unsigned short DO_offset;
/* PINs state */
unsigned char verified_pin[5];
unsigned char pinmode;
unsigned char pinmode_req;
/* ux menus */
char menu[112];
unsigned char ux_pinentry[GPG_MAX_PW_LENGTH];
unsigned char ux_pinLen;
unsigned int ux_key;
unsigned int ux_type;
#ifdef TARGET_NANOS
ux_menu_entry_t ui_dogsays[2];
#endif
#ifdef UI_NANO_X
char ux_buff1[32];
char ux_buff2[32];
char ux_buff3[32];
char ux_buff4[32];
char ux_buff5[32];
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
char ux_buff1[32];
char ux_buff2[32];
char ux_buff3[32];
#endif
#ifdef GPG_LOG
unsigned char log_buffer[32];
#endif
#ifdef GPG_DEBUG
unsigned char print;
#ifdef HAVE_NBGL
char line[112];
unsigned int ux_step;
nbgl_layout_t *layoutCtx;
#endif
#ifdef GPG_LOG
unsigned char log_buffer[256];
#endif
};
typedef struct gpg_v_state_s gpg_v_state_t;
/* --- Errors --- */
#define ERROR(x) ((x) << 16)
/* --- Identifiers --- */
#define ERROR_IO_OFFSET ERROR(1)
#define ERROR_IO_FULL ERROR(2)
/* --- IDentifiers --- */
#define ID_AUTH 1
#define ID_DEC 2
#define ID_SIG 3
#define STATE_ACTIVATE 0x07
#define STATE_ACTIVATE 0x05
#define STATE_TERMINATE 0x03
#define IO_OFFSET_END (unsigned int)-1
#define IO_OFFSET_MARK (unsigned int)-2
#define IO_OFFSET_END (unsigned int) -1
#define IO_OFFSET_MARK (unsigned int) -2
#define PIN_ID_PW1 0x81
#define PIN_ID_PW2 0x82
#define PIN_ID_PW3 0x83
#define PIN_ID_RC 0x84
#define PIN_ID_RC 0x84
#define PIN_MODE_HOST 1
#define PIN_MODE_SCREEN 2
#define PIN_MODE_CONFIRM 3
#define PIN_MODE_TRUST 4
// PIN_MODE_HOST not supported by Ledger App
#define PIN_MODE_SCREEN 0
#define PIN_MODE_CONFIRM 1
#define PIN_MODE_TRUST 2
/* --- CLA --- */
#define CLA_APP_DEF 0x00
#define CLA_APP_SM 0x0C
#define CLA_APP_CHAIN 0x10
#define CLA_APP_CHAIN_SM 0x1C
#define CLA_APP_APDU_PIN PIN_OPR_APDU_CLA
/* --- INS --- */
#define INS_EXIT 0x02
#ifdef GPG_LOG
#define INS_GET_LOG 0x04
#endif
#define INS_SELECT 0xa4
#define INS_TERMINATE_DF 0xe6
#define INS_ACTIVATE_FILE 0x44
#define INS_SELECT_DATA 0xa5
#define INS_GET_DATA 0xca
#define INS_GET_NEXT_DATA 0xcc
#define INS_PUT_DATA 0xda
#define INS_PUT_DATA_ODD 0xdb
#define INS_VERIFY 0x20
#define INS_MSE 0x22
#define INS_EXIT 0x02
#define INS_VERIFY 0x20
#define INS_MSE 0x22
#define INS_CHANGE_REFERENCE_DATA 0x24
#define INS_RESET_RETRY_COUNTER 0x2c
#define INS_GEN_ASYM_KEYPAIR 0x47
#define INS_PSO 0x2a
//#define INS_COMPUTEDIGSIG 0x2a
//#define INS_DECIPHER 0x2a
#define INS_PSO 0x2a
#define INS_RESET_RETRY_COUNTER 0x2c
#define INS_ACTIVATE_FILE 0x44
#define INS_GEN_ASYM_KEYPAIR 0x47
#define INS_GET_CHALLENGE 0x84
#define INS_INTERNAL_AUTHENTICATE 0x88
#define INS_GET_CHALLENGE 0x84
#define INS_GET_RESPONSE 0xc0
/* --- IO constants --- */
#define OFFSET_CLA 0
#define OFFSET_INS 1
#define OFFSET_P1 2
#define OFFSET_P2 3
#define OFFSET_P3 4
#define OFFSET_CDATA 5
#define OFFSET_EXT_CDATA 7
#define SW_OK 0x9000
#define SW_ALGORITHM_UNSUPPORTED 0x9484
#define SW_BYTES_REMAINING_00 0x6100
#define SW_WARNING_STATE_UNCHANGED 0x6200
#define SW_STATE_TERMINATED 0x6285
#define SW_MORE_DATA_AVAILABLE 0x6310
#define SW_WRONG_LENGTH 0x6700
#define SW_LOGICAL_CHANNEL_NOT_SUPPORTED 0x6881
#define SW_SECURE_MESSAGING_NOT_SUPPORTED 0x6882
#define SW_LAST_COMMAND_EXPECTED 0x6883
#define SW_COMMAND_CHAINING_NOT_SUPPORTED 0x6884
#define INS_SELECT 0xa4
#define INS_SELECT_DATA 0xa5
#define INS_GET_RESPONSE 0xc0
#define INS_GET_DATA 0xca
#define INS_GET_NEXT_DATA 0xcc
#define INS_PUT_DATA 0xda
#define INS_PUT_DATA_ODD 0xdb
#define INS_TERMINATE_DF 0xe6
/* --- Error constants --- */
// #define SW_LOGICAL_CHANNEL_NOT_SUPPORTED 0x6881
// #define SW_SECURE_MESSAGING_NOT_SUPPORTED 0x6882
// #define SW_COMMAND_CHAINING_NOT_SUPPORTED 0x6884
// #define SW_SM_DATA_MISSING 0x6987
// #define SW_SM_DATA_INCORRECT 0x6988
#define SW_STATE_TERMINATED 0x6285
#define SW_PWD_NOT_CHECKED 0x63c0
#define SW_MEMORY_FAILURE 0x6581
#define SW_SECURITY_UIF_ISSUE 0x6600
#define SW_WRONG_LENGTH 0x6700
#define SW_LAST_COMMAND_CHAIN_EXPECTED 0x6883
#define SW_SECURITY_STATUS_NOT_SATISFIED 0x6982
#define SW_FILE_INVALID 0x6983
#define SW_PIN_BLOCKED 0x6983
#define SW_DATA_INVALID 0x6984
#define SW_CONDITIONS_NOT_SATISFIED 0x6985
#define SW_COMMAND_NOT_ALLOWED 0x6986
#define SW_APPLET_SELECT_FAILED 0x6999
#define SW_WRONG_DATA 0x6a80
#define SW_FUNC_NOT_SUPPORTED 0x6a81
#define SW_FILE_NOT_FOUND 0x6a82
#define SW_RECORD_NOT_FOUND 0x6a83
#define SW_FILE_FULL 0x6a84
#define SW_INCORRECT_P1P2 0x6a86
#define SW_REFERENCED_DATA_NOT_FOUND 0x6a88
#define SW_WRONG_P1P2 0x6b00
#define SW_CORRECT_LENGTH_00 0x6c00
#define SW_INS_NOT_SUPPORTED 0x6d00
#define SW_CLA_NOT_SUPPORTED 0x6e00
#define SW_UNKNOWN 0x6f00
#define SW_PIN_BLOCKED 0x6983
#define SW_CONDITIONS_NOT_SATISFIED 0x6985
#define SW_WRONG_DATA 0x6a80
#define SW_FILE_NOT_FOUND 0x6a82
#define SW_REFERENCED_DATA_NOT_FOUND 0x6a88
#define SW_WRONG_P1P2 0x6b00
#define SW_INS_NOT_SUPPORTED 0x6d00
#define SW_CLA_NOT_SUPPORTED 0x6e00
#define SW_UNKNOWN 0x6f00
#define SW_CORRECT_BYTES_AVAILABLE 0x6100
#define SW_OK 0x9000
/* --- P1/P2 constants --- */
#define GEN_ASYM_KEY 0x8000
#define SEEDED_MODE 0x01
#define GEN_ASYM_KEY_SEED (GEN_ASYM_KEY | SEEDED_MODE)
#define READ_ASYM_KEY 0x8100
#define PSO_CDS 0x9e9a
#define PSO_DEC 0x8086
#define PSO_ENC 0x8680
#define MSE_SET 0x41
#define GET_RESPONSE 0x00
#define PIN_NOT_VERIFIED 0xFF
#define PIN_VERIFY 0x00
#define SELECT_MAX_INSTANCE 0x02
#define SELECT_SKIP 0x04
#define RESET_RETRY_WITH_PW3 0x02
#define RESET_RETRY_WITH_CODE 0x00
#define PRIME_MODE 0x02
#define CHALLENGE_NOMINAL 0x00
/* --- Keys constants --- */
#define KEY_SIG 0xb6
#define KEY_DEC 0xb8
#define KEY_AUT 0xa4
#define KEY_NB 3
/* --- Padding indicators --- */
#define PAD_RSA 0x00
#define PAD_AES 0x02
#define PAD_ECDH 0xa6
#endif

@ -0,0 +1,53 @@
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "gpg_vars.h"
#include "gpg_ux.h"
#include "usbd_ccid_if.h"
/**
* Reset CCID
*
*/
void ui_CCID_reset(void) {
io_usb_ccid_set_card_inserted(0);
io_usb_ccid_set_card_inserted(1);
}
/**
* Exit app
*
*/
void app_quit(void) {
// exit app here
os_sched_exit(0);
}
/**
* Reset app
*
*/
void app_reset(void) {
unsigned char magic[MAGIC_LENGTH];
explicit_bzero(magic, MAGIC_LENGTH);
nvm_write((void*) (N_gpg_pstate->magic), magic, MAGIC_LENGTH);
gpg_init();
ui_CCID_reset();
ui_init();
}

@ -0,0 +1,44 @@
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_UX_H
#define GPG_UX_H
#define STR(x) #x
#define XSTR(x) STR(x)
#define LABEL_SIG "Signature"
#define LABEL_AUT "Authentication"
#define LABEL_DEC "Decryption"
#define LABEL_RSA2048 "RSA 2048"
#define LABEL_RSA3072 "RSA 3072"
#define LABEL_RSA4096 "RSA 4096"
#define LABEL_SECP256K1 "SECP 256K1"
#define LABEL_SECP256R1 "SECP 256R1"
#define LABEL_Ed25519 "Ed25519"
void ui_CCID_reset(void);
void app_quit(void);
void app_reset(void);
void ui_init(void);
void ui_menu_pinconfirm_display(unsigned int value);
void ui_menu_pinentry_display(unsigned int value);
void ui_menu_uifconfirm_display(unsigned int value);
#endif // GPG_UX_H

@ -1,151 +0,0 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "os.h"
#include "cx.h"
#include "gpg_types.h"
#include "gpg_api.h"
#include "gpg_vars.h"
#include "os_io_seproxyhal.h"
#include "string.h"
#include "glyphs.h"
/* ----------------------------------------------------------------------- */
/* --- Blue UI layout --- */
/* ----------------------------------------------------------------------- */
/* screeen size:
blue; 320x480
nanoS: 128x32
*/
#if 0
static const bagl_element_t const ui_idle_blue[] = {
{{BAGL_RECTANGLE, 0x00, 0, 60, 320, 420, 0, 0,
BAGL_FILL, 0xf9f9f9, 0xf9f9f9,
0, 0},
NULL,
0,
0,
0,
NULL,
NULL,
NULL},
{{BAGL_RECTANGLE, 0x00, 0, 0, 320, 60, 0, 0,
BAGL_FILL, 0x1d2028, 0x1d2028,
0, 0},
NULL,
0,
0,
0,
NULL,
NULL,
NULL},
{{BAGL_LABEL, 0x00, 20, 0, 320, 60, 0, 0,
BAGL_FILL, 0xFFFFFF, 0x1d2028,
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_MIDDLE, 0},
"GPG Card",
0,
0,
0,
NULL,
NULL,
NULL},
{{BAGL_BUTTON | BAGL_FLAG_TOUCHABLE, 0x00, 190, 215, 120, 40, 0, 6,
BAGL_FILL, 0x41ccb4, 0xF9F9F9,
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_CENTER | BAGL_FONT_ALIGNMENT_MIDDLE,
0},
"Exit",
0,
0x37ae99,
0xF9F9F9,
gpg_io_seproxyhal_touch_exit,
NULL,
NULL},
#ifdef GPG_DEBUG
{{BAGL_BUTTON | BAGL_FLAG_TOUCHABLE, 0x00, 20, 215, 120, 40, 0, 6,
BAGL_FILL, 0x41ccb4, 0xF9F9F9,
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_CENTER | BAGL_FONT_ALIGNMENT_MIDDLE,
0},
"Init",
0,
0x37ae99,
0xF9F9F9,
gpg_io_seproxyhal_touch_debug,
NULL,
NULL}
#endif
};
const bagl_element_t ui_idle_nanos[] = {
// type
{{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0,
BAGL_FILL, 0x000000, 0xFFFFFF,
0,
0},
NULL,
0,
0,
0,
NULL,
NULL,
NULL},
{{BAGL_LABELINE, 0x00, 0, 12, 128, 32, 0, 0,
0 , 0xFFFFFF, 0x000000,
BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER,
0 },
"GPGCard",
0,
0,
0,
NULL,
NULL,
NULL },
//{{BAGL_LABELINE , 0x02, 0, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Waiting for requests...", 0, 0, 0, NULL, NULL, NULL },
{{BAGL_ICON , 0x00, 3, 12, 7,
7, 0, 0,
0 , 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CROSS },
NULL,
0,
0,
0,
NULL,
NULL,
NULL },
};
unsigned int gpg_io_seproxyhal_touch_exit(const bagl_element_t *e) {
// Go back to the dashboard
os_sched_exit(0);
return 0; // do not redraw the widget
}
unsigned int ui_idle_blue_button(unsigned int button_mask,
unsigned int button_mask_counter) {
return 0;
}
#endif

@ -1,37 +1,42 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
const char *const C_TEMPLATE_TYPE = "Key type";
const char *const C_TEMPLATE_KEY = "Key";
const char *const C_TEMPLATE_TYPE = "Key type";
const char *const C_TEMPLATE_KEY = "Key";
const char *const C_INVALID_SELECTION = "Invalid selection";
const char *const C_OK = "OK";
const char *const C_OK = "OK";
const char *const C_NOK = "NOK";
const char *const C_WRONG_PIN = "PIN Incorrect";
const char *const C_RIGHT_PIN = "PIN Correct";
const char *const C_WRONG_PIN = "PIN Incorrect";
const char *const C_RIGHT_PIN = "PIN Correct";
const char *const C_PIN_CHANGED = "PIN changed";
const char *const C_PIN_LOCKED = "PIN locked";
const char *const C_PIN_DIFFERS = "2 PINs differs";
const char *const C_PIN_USER = "User PIN";
const char *const C_PIN_ADMIN = "Admin PIN";
const char *const C_PIN_USER = "User PIN";
const char *const C_PIN_ADMIN = "Admin PIN";
const char *const C_VERIFIED = "Verified";
const char *const C_VERIFIED = "Verified";
const char *const C_NOT_VERIFIED = "Not Verified";
const char *const C_ALLOWED = "Allowed";
const char *const C_NOT_ALLOWED = "Not Allowed ";
const char *const C_ALLOWED = "Allowed";
const char *const C_NOT_ALLOWED = "Not Allowed ";
const char *const C_DEFAULT_MODE = "Default mode";
const char *const C_UIF_LOCKED = "UIF locked";
const char *const C_EMPTY = "";

@ -1,17 +1,19 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_UX_MSG_H
#define GPG_UX_MSG_H
@ -26,6 +28,7 @@ extern const char *const C_NOK;
extern const char *const C_WRONG_PIN;
extern const char *const C_RIGHT_PIN;
extern const char *const C_PIN_CHANGED;
extern const char *const C_PIN_LOCKED;
extern const char *const C_PIN_DIFFERS;
extern const char *const C_PIN_USER;
extern const char *const C_PIN_ADMIN;
@ -39,25 +42,29 @@ extern const char *const C_DEFAULT_MODE;
extern const char *const C_UIF_LOCKED;
extern const char *const C_UIF_INVALID;
#define PICSTR(x) ((char *)PIC(x))
extern const char *const C_EMPTY;
#define TEMPLATE_TYPE PICSTR(C_TEMPLATE_TYPE)
#define TEMPLATE_KEY PICSTR(C_TEMPLATE_KEY)
#define PICSTR(x) ((char *) PIC(x))
#define TEMPLATE_TYPE PICSTR(C_TEMPLATE_TYPE)
#define TEMPLATE_KEY PICSTR(C_TEMPLATE_KEY)
#define INVALID_SELECTION PICSTR(C_INVALID_SELECTION)
#define OK PICSTR(C_OK)
#define NOK PICSTR(C_NOK)
#define WRONG_PIN PICSTR(C_WRONG_PIN)
#define RIGHT_PIN PICSTR(C_RIGHT_PIN)
#define PIN_CHANGED PICSTR(C_PIN_CHANGED)
#define PIN_DIFFERS PICSTR(C_PIN_DIFFERS)
#define PIN_USER PICSTR(C_PIN_USER)
#define PIN_ADMIN PICSTR(C_PIN_ADMIN)
#define VERIFIED PICSTR(C_VERIFIED)
#define NOT_VERIFIED PICSTR(C_NOT_VERIFIED)
#define ALLOWED PICSTR(C_ALLOWED)
#define NOT_ALLOWED PICSTR(C_NOT_ALLOWED)
#define DEFAULT_MODE PICSTR(C_DEFAULT_MODE)
#define UIF_LOCKED PICSTR(C_UIF_LOCKED)
#define UIF_INVALID PICSTR(C_UIF_INVALID)
#define OK PICSTR(C_OK)
#define NOK PICSTR(C_NOK)
#define WRONG_PIN PICSTR(C_WRONG_PIN)
#define RIGHT_PIN PICSTR(C_RIGHT_PIN)
#define PIN_CHANGED PICSTR(C_PIN_CHANGED)
#define PIN_LOCKED PICSTR(C_PIN_LOCKED)
#define PIN_DIFFERS PICSTR(C_PIN_DIFFERS)
#define PIN_USER PICSTR(C_PIN_USER)
#define PIN_ADMIN PICSTR(C_PIN_ADMIN)
#define VERIFIED PICSTR(C_VERIFIED)
#define NOT_VERIFIED PICSTR(C_NOT_VERIFIED)
#define ALLOWED PICSTR(C_ALLOWED)
#define NOT_ALLOWED PICSTR(C_NOT_ALLOWED)
#define DEFAULT_MODE PICSTR(C_DEFAULT_MODE)
#define UIF_LOCKED PICSTR(C_UIF_LOCKED)
#define UIF_INVALID PICSTR(C_UIF_INVALID)
#define EMPTY PICSTR(C_EMPTY)
#endif

File diff suppressed because it is too large Load Diff

@ -1,25 +0,0 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef GPG_UX_NANOS_H
#define GPG_UX_NANOS_H
void ui_init(void);
void ui_main_display(unsigned int value);
void ui_menu_pinconfirm_display(unsigned int value);
void ui_menu_pinentry_display(unsigned int value);
void ui_menu_uifconfirm_display(unsigned int value);
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,22 @@
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#include "gpg_vars.h"
gpg_v_state_t G_gpg_vstate;
const gpg_nv_state_t N_state_pic;

@ -1,17 +1,19 @@
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
/*****************************************************************************
* Ledger App OpenPGP.
* (c) 2024 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
#ifndef GPG_VARS_H
#define GPG_VARS_H
@ -19,7 +21,6 @@
#include "os.h"
#include "cx.h"
#include "ux.h"
#include "os_io_seproxyhal.h"
#include "gpg_types.h"
#include "gpg_api.h"
@ -31,25 +32,11 @@ extern const unsigned char C_OID_BRAINPOOL256R1[9];
extern const unsigned char C_OID_BRAINPOOL256T1[9];
extern const unsigned char C_OID_Ed25519[9];
extern const unsigned char C_OID_cv25519[10];
extern const unsigned char C_gen_feature;
extern gpg_v_state_t G_gpg_vstate;
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
extern const gpg_nv_state_t N_state_pic;
#define N_gpg_pstate ((volatile gpg_nv_state_t *)PIC(&N_state_pic))
#else
extern gpg_nv_state_t N_state_pic;
#define N_gpg_pstate ((WIDE gpg_nv_state_t *)PIC(&N_state_pic))
#endif
#ifdef GPG_DEBUG_MAIN
extern int apdu_n;
#endif
extern ux_state_t ux;
#define N_gpg_pstate ((volatile gpg_nv_state_t *) PIC(&N_state_pic))
#ifdef HAVE_RSA
#include "cx_ram.h"
extern union cx_u G_cx;
#endif // HAVE_RSA
#endif

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save